##// END OF EJS Templates
patch: support diff data loss detection and upgrade...
Patrick Mezard -
r10189:e451e599 default
parent child Browse files
Show More
@@ -0,0 +1,46 b''
1 # Extension dedicated to test patch.diff() upgrade modes
2 #
3 #
4 from mercurial import cmdutil, patch, util
5
6 def autodiff(ui, repo, *pats, **opts):
7 diffopts = patch.diffopts(ui, opts)
8 git = opts.get('git', 'no')
9 brokenfiles = set()
10 losedatafn = None
11 if git in ('yes', 'no'):
12 diffopts.git = git == 'yes'
13 diffopts.upgrade = False
14 elif git == 'auto':
15 diffopts.git = False
16 diffopts.upgrade = True
17 elif git == 'warn':
18 diffopts.git = False
19 diffopts.upgrade = True
20 def losedatafn(fn=None, **kwargs):
21 brokenfiles.add(fn)
22 return True
23 elif git == 'abort':
24 diffopts.git = False
25 diffopts.upgrade = True
26 def losedatafn(fn=None, **kwargs):
27 raise util.Abort('losing data for %s' % fn)
28 else:
29 raise util.Abort('--git must be yes, no or auto')
30
31 node1, node2 = cmdutil.revpair(repo, [])
32 m = cmdutil.match(repo, pats, opts)
33 it = patch.diff(repo, node1, node2, match=m, opts=diffopts,
34 losedatafn=losedatafn)
35 for chunk in it:
36 ui.write(chunk)
37 for fn in sorted(brokenfiles):
38 ui.write('data lost for: %s\n' % fn)
39
40 cmdtable = {
41 "autodiff":
42 (autodiff,
43 [('', 'git', '', 'git upgrade mode (yes/no/auto/warn/abort)'),
44 ],
45 '[OPTION]... [FILE]...'),
46 }
@@ -0,0 +1,63 b''
1 #!/bin/sh
2
3 echo "[extensions]" >> $HGRCPATH
4 echo "autodiff=$TESTDIR/autodiff.py" >> $HGRCPATH
5 echo "[diff]" >> $HGRCPATH
6 echo "nodates=1" >> $HGRCPATH
7
8 hg init repo
9 cd repo
10 echo '% make a combination of new, changed and deleted file'
11 echo regular > regular
12 echo rmregular > rmregular
13 touch rmempty
14 echo exec > exec
15 chmod +x exec
16 echo rmexec > rmexec
17 chmod +x rmexec
18 echo setexec > setexec
19 echo unsetexec > unsetexec
20 chmod +x unsetexec
21 echo binary > binary
22 python -c "file('rmbinary', 'wb').write('\0')"
23 hg ci -Am addfiles
24 echo regular >> regular
25 echo newregular >> newregular
26 rm rmempty
27 touch newempty
28 rm rmregular
29 echo exec >> exec
30 echo newexec > newexec
31 chmod +x newexec
32 rm rmexec
33 chmod +x setexec
34 chmod -x unsetexec
35 python -c "file('binary', 'wb').write('\0\0')"
36 python -c "file('newbinary', 'wb').write('\0')"
37 rm rmbinary
38 hg addremove
39
40 echo '% git=no: regular diff for all files'
41 hg autodiff --git=no
42
43 echo '% git=no: git diff for single regular file'
44 hg autodiff --git=yes regular
45
46 echo '% git=auto: regular diff for regular files and removals'
47 hg autodiff --git=auto regular newregular rmregular rmbinary rmexec
48
49 for f in exec newexec setexec unsetexec binary newbinary newempty rmempty; do
50 echo '% git=auto: git diff for' $f
51 hg autodiff --git=auto $f
52 done
53
54 echo '% git=warn: regular diff with data loss warnings'
55 hg autodiff --git=warn
56
57 echo '% git=abort: fail on execute bit change'
58 hg autodiff --git=abort regular setexec
59
60 echo '% git=abort: succeed on regular file'
61 hg autodiff --git=abort regular
62
63 cd ..
@@ -0,0 +1,186 b''
1 % make a combination of new, changed and deleted file
2 adding binary
3 adding exec
4 adding regular
5 adding rmbinary
6 adding rmempty
7 adding rmexec
8 adding rmregular
9 adding setexec
10 adding unsetexec
11 adding newbinary
12 adding newempty
13 adding newexec
14 adding newregular
15 removing rmbinary
16 removing rmempty
17 removing rmexec
18 removing rmregular
19 % git=no: regular diff for all files
20 diff -r b3f053cd7c7f binary
21 Binary file binary has changed
22 diff -r b3f053cd7c7f exec
23 --- a/exec
24 +++ b/exec
25 @@ -1,1 +1,2 @@
26 exec
27 +exec
28 diff -r b3f053cd7c7f newbinary
29 Binary file newbinary has changed
30 diff -r b3f053cd7c7f newexec
31 --- /dev/null
32 +++ b/newexec
33 @@ -0,0 +1,1 @@
34 +newexec
35 diff -r b3f053cd7c7f newregular
36 --- /dev/null
37 +++ b/newregular
38 @@ -0,0 +1,1 @@
39 +newregular
40 diff -r b3f053cd7c7f regular
41 --- a/regular
42 +++ b/regular
43 @@ -1,1 +1,2 @@
44 regular
45 +regular
46 diff -r b3f053cd7c7f rmbinary
47 Binary file rmbinary has changed
48 diff -r b3f053cd7c7f rmexec
49 --- a/rmexec
50 +++ /dev/null
51 @@ -1,1 +0,0 @@
52 -rmexec
53 diff -r b3f053cd7c7f rmregular
54 --- a/rmregular
55 +++ /dev/null
56 @@ -1,1 +0,0 @@
57 -rmregular
58 % git=no: git diff for single regular file
59 diff --git a/regular b/regular
60 --- a/regular
61 +++ b/regular
62 @@ -1,1 +1,2 @@
63 regular
64 +regular
65 % git=auto: regular diff for regular files and removals
66 diff -r b3f053cd7c7f newregular
67 --- /dev/null
68 +++ b/newregular
69 @@ -0,0 +1,1 @@
70 +newregular
71 diff -r b3f053cd7c7f regular
72 --- a/regular
73 +++ b/regular
74 @@ -1,1 +1,2 @@
75 regular
76 +regular
77 diff -r b3f053cd7c7f rmbinary
78 Binary file rmbinary has changed
79 diff -r b3f053cd7c7f rmexec
80 --- a/rmexec
81 +++ /dev/null
82 @@ -1,1 +0,0 @@
83 -rmexec
84 diff -r b3f053cd7c7f rmregular
85 --- a/rmregular
86 +++ /dev/null
87 @@ -1,1 +0,0 @@
88 -rmregular
89 % git=auto: git diff for exec
90 diff -r b3f053cd7c7f exec
91 --- a/exec
92 +++ b/exec
93 @@ -1,1 +1,2 @@
94 exec
95 +exec
96 % git=auto: git diff for newexec
97 diff --git a/newexec b/newexec
98 new file mode 100755
99 --- /dev/null
100 +++ b/newexec
101 @@ -0,0 +1,1 @@
102 +newexec
103 % git=auto: git diff for setexec
104 diff --git a/setexec b/setexec
105 old mode 100644
106 new mode 100755
107 % git=auto: git diff for unsetexec
108 diff --git a/unsetexec b/unsetexec
109 old mode 100755
110 new mode 100644
111 % git=auto: git diff for binary
112 diff --git a/binary b/binary
113 index a9128c283485202893f5af379dd9beccb6e79486..09f370e38f498a462e1ca0faa724559b6630c04f
114 GIT binary patch
115 literal 2
116 Jc${Nk0000200961
117
118 % git=auto: git diff for newbinary
119 diff --git a/newbinary b/newbinary
120 new file mode 100644
121 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d
122 GIT binary patch
123 literal 1
124 Ic${MZ000310RR91
125
126 % git=auto: git diff for newempty
127 diff --git a/newempty b/newempty
128 new file mode 100644
129 % git=auto: git diff for rmempty
130 diff --git a/rmempty b/rmempty
131 deleted file mode 100644
132 % git=warn: regular diff with data loss warnings
133 diff -r b3f053cd7c7f binary
134 Binary file binary has changed
135 diff -r b3f053cd7c7f exec
136 --- a/exec
137 +++ b/exec
138 @@ -1,1 +1,2 @@
139 exec
140 +exec
141 diff -r b3f053cd7c7f newbinary
142 Binary file newbinary has changed
143 diff -r b3f053cd7c7f newexec
144 --- /dev/null
145 +++ b/newexec
146 @@ -0,0 +1,1 @@
147 +newexec
148 diff -r b3f053cd7c7f newregular
149 --- /dev/null
150 +++ b/newregular
151 @@ -0,0 +1,1 @@
152 +newregular
153 diff -r b3f053cd7c7f regular
154 --- a/regular
155 +++ b/regular
156 @@ -1,1 +1,2 @@
157 regular
158 +regular
159 diff -r b3f053cd7c7f rmbinary
160 Binary file rmbinary has changed
161 diff -r b3f053cd7c7f rmexec
162 --- a/rmexec
163 +++ /dev/null
164 @@ -1,1 +0,0 @@
165 -rmexec
166 diff -r b3f053cd7c7f rmregular
167 --- a/rmregular
168 +++ /dev/null
169 @@ -1,1 +0,0 @@
170 -rmregular
171 data lost for: binary
172 data lost for: newbinary
173 data lost for: newempty
174 data lost for: newexec
175 data lost for: rmempty
176 data lost for: setexec
177 data lost for: unsetexec
178 % git=abort: fail on execute bit change
179 abort: losing data for setexec
180 % git=abort: succeed on regular file
181 diff -r b3f053cd7c7f regular
182 --- a/regular
183 +++ b/regular
184 @@ -1,1 +1,2 @@
185 regular
186 +regular
@@ -1,278 +1,281 b''
1 # mdiff.py - diff and patch routines for mercurial
1 # mdiff.py - diff and patch routines for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 import bdiff, mpatch, util
9 import bdiff, mpatch, util
10 import re, struct
10 import re, struct
11
11
12 def splitnewlines(text):
12 def splitnewlines(text):
13 '''like str.splitlines, but only split on newlines.'''
13 '''like str.splitlines, but only split on newlines.'''
14 lines = [l + '\n' for l in text.split('\n')]
14 lines = [l + '\n' for l in text.split('\n')]
15 if lines:
15 if lines:
16 if lines[-1] == '\n':
16 if lines[-1] == '\n':
17 lines.pop()
17 lines.pop()
18 else:
18 else:
19 lines[-1] = lines[-1][:-1]
19 lines[-1] = lines[-1][:-1]
20 return lines
20 return lines
21
21
22 class diffopts(object):
22 class diffopts(object):
23 '''context is the number of context lines
23 '''context is the number of context lines
24 text treats all files as text
24 text treats all files as text
25 showfunc enables diff -p output
25 showfunc enables diff -p output
26 git enables the git extended patch format
26 git enables the git extended patch format
27 nodates removes dates from diff headers
27 nodates removes dates from diff headers
28 ignorews ignores all whitespace changes in the diff
28 ignorews ignores all whitespace changes in the diff
29 ignorewsamount ignores changes in the amount of whitespace
29 ignorewsamount ignores changes in the amount of whitespace
30 ignoreblanklines ignores changes whose lines are all blank'''
30 ignoreblanklines ignores changes whose lines are all blank
31 upgrade generates git diffs to avoid data loss
32 '''
31
33
32 defaults = {
34 defaults = {
33 'context': 3,
35 'context': 3,
34 'text': False,
36 'text': False,
35 'showfunc': False,
37 'showfunc': False,
36 'git': False,
38 'git': False,
37 'nodates': False,
39 'nodates': False,
38 'ignorews': False,
40 'ignorews': False,
39 'ignorewsamount': False,
41 'ignorewsamount': False,
40 'ignoreblanklines': False,
42 'ignoreblanklines': False,
43 'upgrade': False,
41 }
44 }
42
45
43 __slots__ = defaults.keys()
46 __slots__ = defaults.keys()
44
47
45 def __init__(self, **opts):
48 def __init__(self, **opts):
46 for k in self.__slots__:
49 for k in self.__slots__:
47 v = opts.get(k)
50 v = opts.get(k)
48 if v is None:
51 if v is None:
49 v = self.defaults[k]
52 v = self.defaults[k]
50 setattr(self, k, v)
53 setattr(self, k, v)
51
54
52 try:
55 try:
53 self.context = int(self.context)
56 self.context = int(self.context)
54 except ValueError:
57 except ValueError:
55 raise util.Abort(_('diff context lines count must be '
58 raise util.Abort(_('diff context lines count must be '
56 'an integer, not %r') % self.context)
59 'an integer, not %r') % self.context)
57
60
58 def copy(self, **kwargs):
61 def copy(self, **kwargs):
59 opts = dict((k, getattr(self, k)) for k in self.defaults)
62 opts = dict((k, getattr(self, k)) for k in self.defaults)
60 opts.update(kwargs)
63 opts.update(kwargs)
61 return diffopts(**opts)
64 return diffopts(**opts)
62
65
63 defaultopts = diffopts()
66 defaultopts = diffopts()
64
67
65 def wsclean(opts, text, blank=True):
68 def wsclean(opts, text, blank=True):
66 if opts.ignorews:
69 if opts.ignorews:
67 text = re.sub('[ \t]+', '', text)
70 text = re.sub('[ \t]+', '', text)
68 elif opts.ignorewsamount:
71 elif opts.ignorewsamount:
69 text = re.sub('[ \t]+', ' ', text)
72 text = re.sub('[ \t]+', ' ', text)
70 text = re.sub('[ \t]+\n', '\n', text)
73 text = re.sub('[ \t]+\n', '\n', text)
71 if blank and opts.ignoreblanklines:
74 if blank and opts.ignoreblanklines:
72 text = re.sub('\n+', '', text)
75 text = re.sub('\n+', '', text)
73 return text
76 return text
74
77
75 def diffline(revs, a, b, opts):
78 def diffline(revs, a, b, opts):
76 parts = ['diff']
79 parts = ['diff']
77 if opts.git:
80 if opts.git:
78 parts.append('--git')
81 parts.append('--git')
79 if revs and not opts.git:
82 if revs and not opts.git:
80 parts.append(' '.join(["-r %s" % rev for rev in revs]))
83 parts.append(' '.join(["-r %s" % rev for rev in revs]))
81 if opts.git:
84 if opts.git:
82 parts.append('a/%s' % a)
85 parts.append('a/%s' % a)
83 parts.append('b/%s' % b)
86 parts.append('b/%s' % b)
84 else:
87 else:
85 parts.append(a)
88 parts.append(a)
86 return ' '.join(parts) + '\n'
89 return ' '.join(parts) + '\n'
87
90
88 def unidiff(a, ad, b, bd, fn1, fn2, r=None, opts=defaultopts):
91 def unidiff(a, ad, b, bd, fn1, fn2, r=None, opts=defaultopts):
89 def datetag(date, addtab=True):
92 def datetag(date, addtab=True):
90 if not opts.git and not opts.nodates:
93 if not opts.git and not opts.nodates:
91 return '\t%s\n' % date
94 return '\t%s\n' % date
92 if addtab and ' ' in fn1:
95 if addtab and ' ' in fn1:
93 return '\t\n'
96 return '\t\n'
94 return '\n'
97 return '\n'
95
98
96 if not a and not b: return ""
99 if not a and not b: return ""
97 epoch = util.datestr((0, 0))
100 epoch = util.datestr((0, 0))
98
101
99 if not opts.text and (util.binary(a) or util.binary(b)):
102 if not opts.text and (util.binary(a) or util.binary(b)):
100 if a and b and len(a) == len(b) and a == b:
103 if a and b and len(a) == len(b) and a == b:
101 return ""
104 return ""
102 l = ['Binary file %s has changed\n' % fn1]
105 l = ['Binary file %s has changed\n' % fn1]
103 elif not a:
106 elif not a:
104 b = splitnewlines(b)
107 b = splitnewlines(b)
105 if a is None:
108 if a is None:
106 l1 = '--- /dev/null%s' % datetag(epoch, False)
109 l1 = '--- /dev/null%s' % datetag(epoch, False)
107 else:
110 else:
108 l1 = "--- %s%s" % ("a/" + fn1, datetag(ad))
111 l1 = "--- %s%s" % ("a/" + fn1, datetag(ad))
109 l2 = "+++ %s%s" % ("b/" + fn2, datetag(bd))
112 l2 = "+++ %s%s" % ("b/" + fn2, datetag(bd))
110 l3 = "@@ -0,0 +1,%d @@\n" % len(b)
113 l3 = "@@ -0,0 +1,%d @@\n" % len(b)
111 l = [l1, l2, l3] + ["+" + e for e in b]
114 l = [l1, l2, l3] + ["+" + e for e in b]
112 elif not b:
115 elif not b:
113 a = splitnewlines(a)
116 a = splitnewlines(a)
114 l1 = "--- %s%s" % ("a/" + fn1, datetag(ad))
117 l1 = "--- %s%s" % ("a/" + fn1, datetag(ad))
115 if b is None:
118 if b is None:
116 l2 = '+++ /dev/null%s' % datetag(epoch, False)
119 l2 = '+++ /dev/null%s' % datetag(epoch, False)
117 else:
120 else:
118 l2 = "+++ %s%s" % ("b/" + fn2, datetag(bd))
121 l2 = "+++ %s%s" % ("b/" + fn2, datetag(bd))
119 l3 = "@@ -1,%d +0,0 @@\n" % len(a)
122 l3 = "@@ -1,%d +0,0 @@\n" % len(a)
120 l = [l1, l2, l3] + ["-" + e for e in a]
123 l = [l1, l2, l3] + ["-" + e for e in a]
121 else:
124 else:
122 al = splitnewlines(a)
125 al = splitnewlines(a)
123 bl = splitnewlines(b)
126 bl = splitnewlines(b)
124 l = list(bunidiff(a, b, al, bl, "a/" + fn1, "b/" + fn2, opts=opts))
127 l = list(bunidiff(a, b, al, bl, "a/" + fn1, "b/" + fn2, opts=opts))
125 if not l: return ""
128 if not l: return ""
126 # difflib uses a space, rather than a tab
129 # difflib uses a space, rather than a tab
127 l[0] = "%s%s" % (l[0][:-2], datetag(ad))
130 l[0] = "%s%s" % (l[0][:-2], datetag(ad))
128 l[1] = "%s%s" % (l[1][:-2], datetag(bd))
131 l[1] = "%s%s" % (l[1][:-2], datetag(bd))
129
132
130 for ln in xrange(len(l)):
133 for ln in xrange(len(l)):
131 if l[ln][-1] != '\n':
134 if l[ln][-1] != '\n':
132 l[ln] += "\n\ No newline at end of file\n"
135 l[ln] += "\n\ No newline at end of file\n"
133
136
134 if r:
137 if r:
135 l.insert(0, diffline(r, fn1, fn2, opts))
138 l.insert(0, diffline(r, fn1, fn2, opts))
136
139
137 return "".join(l)
140 return "".join(l)
138
141
139 # somewhat self contained replacement for difflib.unified_diff
142 # somewhat self contained replacement for difflib.unified_diff
140 # t1 and t2 are the text to be diffed
143 # t1 and t2 are the text to be diffed
141 # l1 and l2 are the text broken up into lines
144 # l1 and l2 are the text broken up into lines
142 # header1 and header2 are the filenames for the diff output
145 # header1 and header2 are the filenames for the diff output
143 def bunidiff(t1, t2, l1, l2, header1, header2, opts=defaultopts):
146 def bunidiff(t1, t2, l1, l2, header1, header2, opts=defaultopts):
144 def contextend(l, len):
147 def contextend(l, len):
145 ret = l + opts.context
148 ret = l + opts.context
146 if ret > len:
149 if ret > len:
147 ret = len
150 ret = len
148 return ret
151 return ret
149
152
150 def contextstart(l):
153 def contextstart(l):
151 ret = l - opts.context
154 ret = l - opts.context
152 if ret < 0:
155 if ret < 0:
153 return 0
156 return 0
154 return ret
157 return ret
155
158
156 def yieldhunk(hunk, header):
159 def yieldhunk(hunk, header):
157 if header:
160 if header:
158 for x in header:
161 for x in header:
159 yield x
162 yield x
160 (astart, a2, bstart, b2, delta) = hunk
163 (astart, a2, bstart, b2, delta) = hunk
161 aend = contextend(a2, len(l1))
164 aend = contextend(a2, len(l1))
162 alen = aend - astart
165 alen = aend - astart
163 blen = b2 - bstart + aend - a2
166 blen = b2 - bstart + aend - a2
164
167
165 func = ""
168 func = ""
166 if opts.showfunc:
169 if opts.showfunc:
167 # walk backwards from the start of the context
170 # walk backwards from the start of the context
168 # to find a line starting with an alphanumeric char.
171 # to find a line starting with an alphanumeric char.
169 for x in xrange(astart - 1, -1, -1):
172 for x in xrange(astart - 1, -1, -1):
170 t = l1[x].rstrip()
173 t = l1[x].rstrip()
171 if funcre.match(t):
174 if funcre.match(t):
172 func = ' ' + t[:40]
175 func = ' ' + t[:40]
173 break
176 break
174
177
175 yield "@@ -%d,%d +%d,%d @@%s\n" % (astart + 1, alen,
178 yield "@@ -%d,%d +%d,%d @@%s\n" % (astart + 1, alen,
176 bstart + 1, blen, func)
179 bstart + 1, blen, func)
177 for x in delta:
180 for x in delta:
178 yield x
181 yield x
179 for x in xrange(a2, aend):
182 for x in xrange(a2, aend):
180 yield ' ' + l1[x]
183 yield ' ' + l1[x]
181
184
182 header = [ "--- %s\t\n" % header1, "+++ %s\t\n" % header2 ]
185 header = [ "--- %s\t\n" % header1, "+++ %s\t\n" % header2 ]
183
186
184 if opts.showfunc:
187 if opts.showfunc:
185 funcre = re.compile('\w')
188 funcre = re.compile('\w')
186
189
187 # bdiff.blocks gives us the matching sequences in the files. The loop
190 # bdiff.blocks gives us the matching sequences in the files. The loop
188 # below finds the spaces between those matching sequences and translates
191 # below finds the spaces between those matching sequences and translates
189 # them into diff output.
192 # them into diff output.
190 #
193 #
191 if opts.ignorews or opts.ignorewsamount:
194 if opts.ignorews or opts.ignorewsamount:
192 t1 = wsclean(opts, t1, False)
195 t1 = wsclean(opts, t1, False)
193 t2 = wsclean(opts, t2, False)
196 t2 = wsclean(opts, t2, False)
194
197
195 diff = bdiff.blocks(t1, t2)
198 diff = bdiff.blocks(t1, t2)
196 hunk = None
199 hunk = None
197 for i, s1 in enumerate(diff):
200 for i, s1 in enumerate(diff):
198 # The first match is special.
201 # The first match is special.
199 # we've either found a match starting at line 0 or a match later
202 # we've either found a match starting at line 0 or a match later
200 # in the file. If it starts later, old and new below will both be
203 # in the file. If it starts later, old and new below will both be
201 # empty and we'll continue to the next match.
204 # empty and we'll continue to the next match.
202 if i > 0:
205 if i > 0:
203 s = diff[i-1]
206 s = diff[i-1]
204 else:
207 else:
205 s = [0, 0, 0, 0]
208 s = [0, 0, 0, 0]
206 delta = []
209 delta = []
207 a1 = s[1]
210 a1 = s[1]
208 a2 = s1[0]
211 a2 = s1[0]
209 b1 = s[3]
212 b1 = s[3]
210 b2 = s1[2]
213 b2 = s1[2]
211
214
212 old = l1[a1:a2]
215 old = l1[a1:a2]
213 new = l2[b1:b2]
216 new = l2[b1:b2]
214
217
215 # bdiff sometimes gives huge matches past eof, this check eats them,
218 # bdiff sometimes gives huge matches past eof, this check eats them,
216 # and deals with the special first match case described above
219 # and deals with the special first match case described above
217 if not old and not new:
220 if not old and not new:
218 continue
221 continue
219
222
220 if opts.ignoreblanklines:
223 if opts.ignoreblanklines:
221 if wsclean(opts, "".join(old)) == wsclean(opts, "".join(new)):
224 if wsclean(opts, "".join(old)) == wsclean(opts, "".join(new)):
222 continue
225 continue
223
226
224 astart = contextstart(a1)
227 astart = contextstart(a1)
225 bstart = contextstart(b1)
228 bstart = contextstart(b1)
226 prev = None
229 prev = None
227 if hunk:
230 if hunk:
228 # join with the previous hunk if it falls inside the context
231 # join with the previous hunk if it falls inside the context
229 if astart < hunk[1] + opts.context + 1:
232 if astart < hunk[1] + opts.context + 1:
230 prev = hunk
233 prev = hunk
231 astart = hunk[1]
234 astart = hunk[1]
232 bstart = hunk[3]
235 bstart = hunk[3]
233 else:
236 else:
234 for x in yieldhunk(hunk, header):
237 for x in yieldhunk(hunk, header):
235 yield x
238 yield x
236 # we only want to yield the header if the files differ, and
239 # we only want to yield the header if the files differ, and
237 # we only want to yield it once.
240 # we only want to yield it once.
238 header = None
241 header = None
239 if prev:
242 if prev:
240 # we've joined the previous hunk, record the new ending points.
243 # we've joined the previous hunk, record the new ending points.
241 hunk[1] = a2
244 hunk[1] = a2
242 hunk[3] = b2
245 hunk[3] = b2
243 delta = hunk[4]
246 delta = hunk[4]
244 else:
247 else:
245 # create a new hunk
248 # create a new hunk
246 hunk = [ astart, a2, bstart, b2, delta ]
249 hunk = [ astart, a2, bstart, b2, delta ]
247
250
248 delta[len(delta):] = [ ' ' + x for x in l1[astart:a1] ]
251 delta[len(delta):] = [ ' ' + x for x in l1[astart:a1] ]
249 delta[len(delta):] = [ '-' + x for x in old ]
252 delta[len(delta):] = [ '-' + x for x in old ]
250 delta[len(delta):] = [ '+' + x for x in new ]
253 delta[len(delta):] = [ '+' + x for x in new ]
251
254
252 if hunk:
255 if hunk:
253 for x in yieldhunk(hunk, header):
256 for x in yieldhunk(hunk, header):
254 yield x
257 yield x
255
258
256 def patchtext(bin):
259 def patchtext(bin):
257 pos = 0
260 pos = 0
258 t = []
261 t = []
259 while pos < len(bin):
262 while pos < len(bin):
260 p1, p2, l = struct.unpack(">lll", bin[pos:pos + 12])
263 p1, p2, l = struct.unpack(">lll", bin[pos:pos + 12])
261 pos += 12
264 pos += 12
262 t.append(bin[pos:pos + l])
265 t.append(bin[pos:pos + l])
263 pos += l
266 pos += l
264 return "".join(t)
267 return "".join(t)
265
268
266 def patch(a, bin):
269 def patch(a, bin):
267 return mpatch.patches(a, [bin])
270 return mpatch.patches(a, [bin])
268
271
269 # similar to difflib.SequenceMatcher.get_matching_blocks
272 # similar to difflib.SequenceMatcher.get_matching_blocks
270 def get_matching_blocks(a, b):
273 def get_matching_blocks(a, b):
271 return [(d[0], d[2], d[1] - d[0]) for d in bdiff.blocks(a, b)]
274 return [(d[0], d[2], d[1] - d[0]) for d in bdiff.blocks(a, b)]
272
275
273 def trivialdiffheader(length):
276 def trivialdiffheader(length):
274 return struct.pack(">lll", 0, 0, length)
277 return struct.pack(">lll", 0, 0, length)
275
278
276 patches = mpatch.patches
279 patches = mpatch.patches
277 patchedsize = mpatch.patchedsize
280 patchedsize = mpatch.patchedsize
278 textdiff = bdiff.bdiff
281 textdiff = bdiff.bdiff
@@ -1,1473 +1,1529 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2, incorporated herein by reference.
8
8
9 from i18n import _
9 from i18n import _
10 from node import hex, nullid, short
10 from node import hex, nullid, short
11 import base85, cmdutil, mdiff, util, diffhelpers, copies
11 import base85, cmdutil, mdiff, util, diffhelpers, copies
12 import cStringIO, email.Parser, os, re
12 import cStringIO, email.Parser, os, re
13 import sys, tempfile, zlib
13 import sys, tempfile, zlib
14
14
15 gitre = re.compile('diff --git a/(.*) b/(.*)')
15 gitre = re.compile('diff --git a/(.*) b/(.*)')
16
16
17 class PatchError(Exception):
17 class PatchError(Exception):
18 pass
18 pass
19
19
20 class NoHunks(PatchError):
20 class NoHunks(PatchError):
21 pass
21 pass
22
22
23 # helper functions
23 # helper functions
24
24
25 def copyfile(src, dst, basedir):
25 def copyfile(src, dst, basedir):
26 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
26 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
27 if os.path.exists(absdst):
27 if os.path.exists(absdst):
28 raise util.Abort(_("cannot create %s: destination already exists") %
28 raise util.Abort(_("cannot create %s: destination already exists") %
29 dst)
29 dst)
30
30
31 dstdir = os.path.dirname(absdst)
31 dstdir = os.path.dirname(absdst)
32 if dstdir and not os.path.isdir(dstdir):
32 if dstdir and not os.path.isdir(dstdir):
33 try:
33 try:
34 os.makedirs(dstdir)
34 os.makedirs(dstdir)
35 except IOError:
35 except IOError:
36 raise util.Abort(
36 raise util.Abort(
37 _("cannot create %s: unable to create destination directory")
37 _("cannot create %s: unable to create destination directory")
38 % dst)
38 % dst)
39
39
40 util.copyfile(abssrc, absdst)
40 util.copyfile(abssrc, absdst)
41
41
42 # public functions
42 # public functions
43
43
44 def extract(ui, fileobj):
44 def extract(ui, fileobj):
45 '''extract patch from data read from fileobj.
45 '''extract patch from data read from fileobj.
46
46
47 patch can be a normal patch or contained in an email message.
47 patch can be a normal patch or contained in an email message.
48
48
49 return tuple (filename, message, user, date, node, p1, p2).
49 return tuple (filename, message, user, date, node, p1, p2).
50 Any item in the returned tuple can be None. If filename is None,
50 Any item in the returned tuple can be None. If filename is None,
51 fileobj did not contain a patch. Caller must unlink filename when done.'''
51 fileobj did not contain a patch. Caller must unlink filename when done.'''
52
52
53 # attempt to detect the start of a patch
53 # attempt to detect the start of a patch
54 # (this heuristic is borrowed from quilt)
54 # (this heuristic is borrowed from quilt)
55 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
55 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
56 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
56 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
57 r'(---|\*\*\*)[ \t])', re.MULTILINE)
57 r'(---|\*\*\*)[ \t])', re.MULTILINE)
58
58
59 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
59 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
60 tmpfp = os.fdopen(fd, 'w')
60 tmpfp = os.fdopen(fd, 'w')
61 try:
61 try:
62 msg = email.Parser.Parser().parse(fileobj)
62 msg = email.Parser.Parser().parse(fileobj)
63
63
64 subject = msg['Subject']
64 subject = msg['Subject']
65 user = msg['From']
65 user = msg['From']
66 if not subject and not user:
66 if not subject and not user:
67 # Not an email, restore parsed headers if any
67 # Not an email, restore parsed headers if any
68 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
68 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
69
69
70 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
70 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
71 # should try to parse msg['Date']
71 # should try to parse msg['Date']
72 date = None
72 date = None
73 nodeid = None
73 nodeid = None
74 branch = None
74 branch = None
75 parents = []
75 parents = []
76
76
77 if subject:
77 if subject:
78 if subject.startswith('[PATCH'):
78 if subject.startswith('[PATCH'):
79 pend = subject.find(']')
79 pend = subject.find(']')
80 if pend >= 0:
80 if pend >= 0:
81 subject = subject[pend+1:].lstrip()
81 subject = subject[pend+1:].lstrip()
82 subject = subject.replace('\n\t', ' ')
82 subject = subject.replace('\n\t', ' ')
83 ui.debug('Subject: %s\n' % subject)
83 ui.debug('Subject: %s\n' % subject)
84 if user:
84 if user:
85 ui.debug('From: %s\n' % user)
85 ui.debug('From: %s\n' % user)
86 diffs_seen = 0
86 diffs_seen = 0
87 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
87 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
88 message = ''
88 message = ''
89 for part in msg.walk():
89 for part in msg.walk():
90 content_type = part.get_content_type()
90 content_type = part.get_content_type()
91 ui.debug('Content-Type: %s\n' % content_type)
91 ui.debug('Content-Type: %s\n' % content_type)
92 if content_type not in ok_types:
92 if content_type not in ok_types:
93 continue
93 continue
94 payload = part.get_payload(decode=True)
94 payload = part.get_payload(decode=True)
95 m = diffre.search(payload)
95 m = diffre.search(payload)
96 if m:
96 if m:
97 hgpatch = False
97 hgpatch = False
98 ignoretext = False
98 ignoretext = False
99
99
100 ui.debug('found patch at byte %d\n' % m.start(0))
100 ui.debug('found patch at byte %d\n' % m.start(0))
101 diffs_seen += 1
101 diffs_seen += 1
102 cfp = cStringIO.StringIO()
102 cfp = cStringIO.StringIO()
103 for line in payload[:m.start(0)].splitlines():
103 for line in payload[:m.start(0)].splitlines():
104 if line.startswith('# HG changeset patch'):
104 if line.startswith('# HG changeset patch'):
105 ui.debug('patch generated by hg export\n')
105 ui.debug('patch generated by hg export\n')
106 hgpatch = True
106 hgpatch = True
107 # drop earlier commit message content
107 # drop earlier commit message content
108 cfp.seek(0)
108 cfp.seek(0)
109 cfp.truncate()
109 cfp.truncate()
110 subject = None
110 subject = None
111 elif hgpatch:
111 elif hgpatch:
112 if line.startswith('# User '):
112 if line.startswith('# User '):
113 user = line[7:]
113 user = line[7:]
114 ui.debug('From: %s\n' % user)
114 ui.debug('From: %s\n' % user)
115 elif line.startswith("# Date "):
115 elif line.startswith("# Date "):
116 date = line[7:]
116 date = line[7:]
117 elif line.startswith("# Branch "):
117 elif line.startswith("# Branch "):
118 branch = line[9:]
118 branch = line[9:]
119 elif line.startswith("# Node ID "):
119 elif line.startswith("# Node ID "):
120 nodeid = line[10:]
120 nodeid = line[10:]
121 elif line.startswith("# Parent "):
121 elif line.startswith("# Parent "):
122 parents.append(line[10:])
122 parents.append(line[10:])
123 elif line == '---' and gitsendmail:
123 elif line == '---' and gitsendmail:
124 ignoretext = True
124 ignoretext = True
125 if not line.startswith('# ') and not ignoretext:
125 if not line.startswith('# ') and not ignoretext:
126 cfp.write(line)
126 cfp.write(line)
127 cfp.write('\n')
127 cfp.write('\n')
128 message = cfp.getvalue()
128 message = cfp.getvalue()
129 if tmpfp:
129 if tmpfp:
130 tmpfp.write(payload)
130 tmpfp.write(payload)
131 if not payload.endswith('\n'):
131 if not payload.endswith('\n'):
132 tmpfp.write('\n')
132 tmpfp.write('\n')
133 elif not diffs_seen and message and content_type == 'text/plain':
133 elif not diffs_seen and message and content_type == 'text/plain':
134 message += '\n' + payload
134 message += '\n' + payload
135 except:
135 except:
136 tmpfp.close()
136 tmpfp.close()
137 os.unlink(tmpname)
137 os.unlink(tmpname)
138 raise
138 raise
139
139
140 if subject and not message.startswith(subject):
140 if subject and not message.startswith(subject):
141 message = '%s\n%s' % (subject, message)
141 message = '%s\n%s' % (subject, message)
142 tmpfp.close()
142 tmpfp.close()
143 if not diffs_seen:
143 if not diffs_seen:
144 os.unlink(tmpname)
144 os.unlink(tmpname)
145 return None, message, user, date, branch, None, None, None
145 return None, message, user, date, branch, None, None, None
146 p1 = parents and parents.pop(0) or None
146 p1 = parents and parents.pop(0) or None
147 p2 = parents and parents.pop(0) or None
147 p2 = parents and parents.pop(0) or None
148 return tmpname, message, user, date, branch, nodeid, p1, p2
148 return tmpname, message, user, date, branch, nodeid, p1, p2
149
149
150 GP_PATCH = 1 << 0 # we have to run patch
150 GP_PATCH = 1 << 0 # we have to run patch
151 GP_FILTER = 1 << 1 # there's some copy/rename operation
151 GP_FILTER = 1 << 1 # there's some copy/rename operation
152 GP_BINARY = 1 << 2 # there's a binary patch
152 GP_BINARY = 1 << 2 # there's a binary patch
153
153
154 class patchmeta(object):
154 class patchmeta(object):
155 """Patched file metadata
155 """Patched file metadata
156
156
157 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
157 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
158 or COPY. 'path' is patched file path. 'oldpath' is set to the
158 or COPY. 'path' is patched file path. 'oldpath' is set to the
159 origin file when 'op' is either COPY or RENAME, None otherwise. If
159 origin file when 'op' is either COPY or RENAME, None otherwise. If
160 file mode is changed, 'mode' is a tuple (islink, isexec) where
160 file mode is changed, 'mode' is a tuple (islink, isexec) where
161 'islink' is True if the file is a symlink and 'isexec' is True if
161 'islink' is True if the file is a symlink and 'isexec' is True if
162 the file is executable. Otherwise, 'mode' is None.
162 the file is executable. Otherwise, 'mode' is None.
163 """
163 """
164 def __init__(self, path):
164 def __init__(self, path):
165 self.path = path
165 self.path = path
166 self.oldpath = None
166 self.oldpath = None
167 self.mode = None
167 self.mode = None
168 self.op = 'MODIFY'
168 self.op = 'MODIFY'
169 self.lineno = 0
169 self.lineno = 0
170 self.binary = False
170 self.binary = False
171
171
172 def setmode(self, mode):
172 def setmode(self, mode):
173 islink = mode & 020000
173 islink = mode & 020000
174 isexec = mode & 0100
174 isexec = mode & 0100
175 self.mode = (islink, isexec)
175 self.mode = (islink, isexec)
176
176
177 def readgitpatch(lr):
177 def readgitpatch(lr):
178 """extract git-style metadata about patches from <patchname>"""
178 """extract git-style metadata about patches from <patchname>"""
179
179
180 # Filter patch for git information
180 # Filter patch for git information
181 gp = None
181 gp = None
182 gitpatches = []
182 gitpatches = []
183 # Can have a git patch with only metadata, causing patch to complain
183 # Can have a git patch with only metadata, causing patch to complain
184 dopatch = 0
184 dopatch = 0
185
185
186 lineno = 0
186 lineno = 0
187 for line in lr:
187 for line in lr:
188 lineno += 1
188 lineno += 1
189 line = line.rstrip(' \r\n')
189 line = line.rstrip(' \r\n')
190 if line.startswith('diff --git'):
190 if line.startswith('diff --git'):
191 m = gitre.match(line)
191 m = gitre.match(line)
192 if m:
192 if m:
193 if gp:
193 if gp:
194 gitpatches.append(gp)
194 gitpatches.append(gp)
195 dst = m.group(2)
195 dst = m.group(2)
196 gp = patchmeta(dst)
196 gp = patchmeta(dst)
197 gp.lineno = lineno
197 gp.lineno = lineno
198 elif gp:
198 elif gp:
199 if line.startswith('--- '):
199 if line.startswith('--- '):
200 if gp.op in ('COPY', 'RENAME'):
200 if gp.op in ('COPY', 'RENAME'):
201 dopatch |= GP_FILTER
201 dopatch |= GP_FILTER
202 gitpatches.append(gp)
202 gitpatches.append(gp)
203 gp = None
203 gp = None
204 dopatch |= GP_PATCH
204 dopatch |= GP_PATCH
205 continue
205 continue
206 if line.startswith('rename from '):
206 if line.startswith('rename from '):
207 gp.op = 'RENAME'
207 gp.op = 'RENAME'
208 gp.oldpath = line[12:]
208 gp.oldpath = line[12:]
209 elif line.startswith('rename to '):
209 elif line.startswith('rename to '):
210 gp.path = line[10:]
210 gp.path = line[10:]
211 elif line.startswith('copy from '):
211 elif line.startswith('copy from '):
212 gp.op = 'COPY'
212 gp.op = 'COPY'
213 gp.oldpath = line[10:]
213 gp.oldpath = line[10:]
214 elif line.startswith('copy to '):
214 elif line.startswith('copy to '):
215 gp.path = line[8:]
215 gp.path = line[8:]
216 elif line.startswith('deleted file'):
216 elif line.startswith('deleted file'):
217 gp.op = 'DELETE'
217 gp.op = 'DELETE'
218 # is the deleted file a symlink?
218 # is the deleted file a symlink?
219 gp.setmode(int(line[-6:], 8))
219 gp.setmode(int(line[-6:], 8))
220 elif line.startswith('new file mode '):
220 elif line.startswith('new file mode '):
221 gp.op = 'ADD'
221 gp.op = 'ADD'
222 gp.setmode(int(line[-6:], 8))
222 gp.setmode(int(line[-6:], 8))
223 elif line.startswith('new mode '):
223 elif line.startswith('new mode '):
224 gp.setmode(int(line[-6:], 8))
224 gp.setmode(int(line[-6:], 8))
225 elif line.startswith('GIT binary patch'):
225 elif line.startswith('GIT binary patch'):
226 dopatch |= GP_BINARY
226 dopatch |= GP_BINARY
227 gp.binary = True
227 gp.binary = True
228 if gp:
228 if gp:
229 gitpatches.append(gp)
229 gitpatches.append(gp)
230
230
231 if not gitpatches:
231 if not gitpatches:
232 dopatch = GP_PATCH
232 dopatch = GP_PATCH
233
233
234 return (dopatch, gitpatches)
234 return (dopatch, gitpatches)
235
235
236 class linereader(object):
236 class linereader(object):
237 # simple class to allow pushing lines back into the input stream
237 # simple class to allow pushing lines back into the input stream
238 def __init__(self, fp, textmode=False):
238 def __init__(self, fp, textmode=False):
239 self.fp = fp
239 self.fp = fp
240 self.buf = []
240 self.buf = []
241 self.textmode = textmode
241 self.textmode = textmode
242 self.eol = None
242 self.eol = None
243
243
244 def push(self, line):
244 def push(self, line):
245 if line is not None:
245 if line is not None:
246 self.buf.append(line)
246 self.buf.append(line)
247
247
248 def readline(self):
248 def readline(self):
249 if self.buf:
249 if self.buf:
250 l = self.buf[0]
250 l = self.buf[0]
251 del self.buf[0]
251 del self.buf[0]
252 return l
252 return l
253 l = self.fp.readline()
253 l = self.fp.readline()
254 if not self.eol:
254 if not self.eol:
255 if l.endswith('\r\n'):
255 if l.endswith('\r\n'):
256 self.eol = '\r\n'
256 self.eol = '\r\n'
257 elif l.endswith('\n'):
257 elif l.endswith('\n'):
258 self.eol = '\n'
258 self.eol = '\n'
259 if self.textmode and l.endswith('\r\n'):
259 if self.textmode and l.endswith('\r\n'):
260 l = l[:-2] + '\n'
260 l = l[:-2] + '\n'
261 return l
261 return l
262
262
263 def __iter__(self):
263 def __iter__(self):
264 while 1:
264 while 1:
265 l = self.readline()
265 l = self.readline()
266 if not l:
266 if not l:
267 break
267 break
268 yield l
268 yield l
269
269
270 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
270 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
271 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
271 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
272 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
272 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
273 eolmodes = ['strict', 'crlf', 'lf', 'auto']
273 eolmodes = ['strict', 'crlf', 'lf', 'auto']
274
274
275 class patchfile(object):
275 class patchfile(object):
276 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
276 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
277 self.fname = fname
277 self.fname = fname
278 self.eolmode = eolmode
278 self.eolmode = eolmode
279 self.eol = None
279 self.eol = None
280 self.opener = opener
280 self.opener = opener
281 self.ui = ui
281 self.ui = ui
282 self.lines = []
282 self.lines = []
283 self.exists = False
283 self.exists = False
284 self.missing = missing
284 self.missing = missing
285 if not missing:
285 if not missing:
286 try:
286 try:
287 self.lines = self.readlines(fname)
287 self.lines = self.readlines(fname)
288 self.exists = True
288 self.exists = True
289 except IOError:
289 except IOError:
290 pass
290 pass
291 else:
291 else:
292 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
292 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
293
293
294 self.hash = {}
294 self.hash = {}
295 self.dirty = 0
295 self.dirty = 0
296 self.offset = 0
296 self.offset = 0
297 self.skew = 0
297 self.skew = 0
298 self.rej = []
298 self.rej = []
299 self.fileprinted = False
299 self.fileprinted = False
300 self.printfile(False)
300 self.printfile(False)
301 self.hunks = 0
301 self.hunks = 0
302
302
303 def readlines(self, fname):
303 def readlines(self, fname):
304 if os.path.islink(fname):
304 if os.path.islink(fname):
305 return [os.readlink(fname)]
305 return [os.readlink(fname)]
306 fp = self.opener(fname, 'r')
306 fp = self.opener(fname, 'r')
307 try:
307 try:
308 lr = linereader(fp, self.eolmode != 'strict')
308 lr = linereader(fp, self.eolmode != 'strict')
309 lines = list(lr)
309 lines = list(lr)
310 self.eol = lr.eol
310 self.eol = lr.eol
311 return lines
311 return lines
312 finally:
312 finally:
313 fp.close()
313 fp.close()
314
314
315 def writelines(self, fname, lines):
315 def writelines(self, fname, lines):
316 # Ensure supplied data ends in fname, being a regular file or
316 # Ensure supplied data ends in fname, being a regular file or
317 # a symlink. updatedir() will -too magically- take care of
317 # a symlink. updatedir() will -too magically- take care of
318 # setting it to the proper type afterwards.
318 # setting it to the proper type afterwards.
319 islink = os.path.islink(fname)
319 islink = os.path.islink(fname)
320 if islink:
320 if islink:
321 fp = cStringIO.StringIO()
321 fp = cStringIO.StringIO()
322 else:
322 else:
323 fp = self.opener(fname, 'w')
323 fp = self.opener(fname, 'w')
324 try:
324 try:
325 if self.eolmode == 'auto':
325 if self.eolmode == 'auto':
326 eol = self.eol
326 eol = self.eol
327 elif self.eolmode == 'crlf':
327 elif self.eolmode == 'crlf':
328 eol = '\r\n'
328 eol = '\r\n'
329 else:
329 else:
330 eol = '\n'
330 eol = '\n'
331
331
332 if self.eolmode != 'strict' and eol and eol != '\n':
332 if self.eolmode != 'strict' and eol and eol != '\n':
333 for l in lines:
333 for l in lines:
334 if l and l[-1] == '\n':
334 if l and l[-1] == '\n':
335 l = l[:-1] + eol
335 l = l[:-1] + eol
336 fp.write(l)
336 fp.write(l)
337 else:
337 else:
338 fp.writelines(lines)
338 fp.writelines(lines)
339 if islink:
339 if islink:
340 self.opener.symlink(fp.getvalue(), fname)
340 self.opener.symlink(fp.getvalue(), fname)
341 finally:
341 finally:
342 fp.close()
342 fp.close()
343
343
344 def unlink(self, fname):
344 def unlink(self, fname):
345 os.unlink(fname)
345 os.unlink(fname)
346
346
347 def printfile(self, warn):
347 def printfile(self, warn):
348 if self.fileprinted:
348 if self.fileprinted:
349 return
349 return
350 if warn or self.ui.verbose:
350 if warn or self.ui.verbose:
351 self.fileprinted = True
351 self.fileprinted = True
352 s = _("patching file %s\n") % self.fname
352 s = _("patching file %s\n") % self.fname
353 if warn:
353 if warn:
354 self.ui.warn(s)
354 self.ui.warn(s)
355 else:
355 else:
356 self.ui.note(s)
356 self.ui.note(s)
357
357
358
358
359 def findlines(self, l, linenum):
359 def findlines(self, l, linenum):
360 # looks through the hash and finds candidate lines. The
360 # looks through the hash and finds candidate lines. The
361 # result is a list of line numbers sorted based on distance
361 # result is a list of line numbers sorted based on distance
362 # from linenum
362 # from linenum
363
363
364 cand = self.hash.get(l, [])
364 cand = self.hash.get(l, [])
365 if len(cand) > 1:
365 if len(cand) > 1:
366 # resort our list of potentials forward then back.
366 # resort our list of potentials forward then back.
367 cand.sort(key=lambda x: abs(x - linenum))
367 cand.sort(key=lambda x: abs(x - linenum))
368 return cand
368 return cand
369
369
370 def hashlines(self):
370 def hashlines(self):
371 self.hash = {}
371 self.hash = {}
372 for x, s in enumerate(self.lines):
372 for x, s in enumerate(self.lines):
373 self.hash.setdefault(s, []).append(x)
373 self.hash.setdefault(s, []).append(x)
374
374
375 def write_rej(self):
375 def write_rej(self):
376 # our rejects are a little different from patch(1). This always
376 # our rejects are a little different from patch(1). This always
377 # creates rejects in the same form as the original patch. A file
377 # creates rejects in the same form as the original patch. A file
378 # header is inserted so that you can run the reject through patch again
378 # header is inserted so that you can run the reject through patch again
379 # without having to type the filename.
379 # without having to type the filename.
380
380
381 if not self.rej:
381 if not self.rej:
382 return
382 return
383
383
384 fname = self.fname + ".rej"
384 fname = self.fname + ".rej"
385 self.ui.warn(
385 self.ui.warn(
386 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
386 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
387 (len(self.rej), self.hunks, fname))
387 (len(self.rej), self.hunks, fname))
388
388
389 def rejlines():
389 def rejlines():
390 base = os.path.basename(self.fname)
390 base = os.path.basename(self.fname)
391 yield "--- %s\n+++ %s\n" % (base, base)
391 yield "--- %s\n+++ %s\n" % (base, base)
392 for x in self.rej:
392 for x in self.rej:
393 for l in x.hunk:
393 for l in x.hunk:
394 yield l
394 yield l
395 if l[-1] != '\n':
395 if l[-1] != '\n':
396 yield "\n\ No newline at end of file\n"
396 yield "\n\ No newline at end of file\n"
397
397
398 self.writelines(fname, rejlines())
398 self.writelines(fname, rejlines())
399
399
400 def write(self, dest=None):
400 def write(self, dest=None):
401 if not self.dirty:
401 if not self.dirty:
402 return
402 return
403 if not dest:
403 if not dest:
404 dest = self.fname
404 dest = self.fname
405 self.writelines(dest, self.lines)
405 self.writelines(dest, self.lines)
406
406
407 def close(self):
407 def close(self):
408 self.write()
408 self.write()
409 self.write_rej()
409 self.write_rej()
410
410
411 def apply(self, h):
411 def apply(self, h):
412 if not h.complete():
412 if not h.complete():
413 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
413 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
414 (h.number, h.desc, len(h.a), h.lena, len(h.b),
414 (h.number, h.desc, len(h.a), h.lena, len(h.b),
415 h.lenb))
415 h.lenb))
416
416
417 self.hunks += 1
417 self.hunks += 1
418
418
419 if self.missing:
419 if self.missing:
420 self.rej.append(h)
420 self.rej.append(h)
421 return -1
421 return -1
422
422
423 if self.exists and h.createfile():
423 if self.exists and h.createfile():
424 self.ui.warn(_("file %s already exists\n") % self.fname)
424 self.ui.warn(_("file %s already exists\n") % self.fname)
425 self.rej.append(h)
425 self.rej.append(h)
426 return -1
426 return -1
427
427
428 if isinstance(h, binhunk):
428 if isinstance(h, binhunk):
429 if h.rmfile():
429 if h.rmfile():
430 self.unlink(self.fname)
430 self.unlink(self.fname)
431 else:
431 else:
432 self.lines[:] = h.new()
432 self.lines[:] = h.new()
433 self.offset += len(h.new())
433 self.offset += len(h.new())
434 self.dirty = 1
434 self.dirty = 1
435 return 0
435 return 0
436
436
437 horig = h
437 horig = h
438 if (self.eolmode in ('crlf', 'lf')
438 if (self.eolmode in ('crlf', 'lf')
439 or self.eolmode == 'auto' and self.eol):
439 or self.eolmode == 'auto' and self.eol):
440 # If new eols are going to be normalized, then normalize
440 # If new eols are going to be normalized, then normalize
441 # hunk data before patching. Otherwise, preserve input
441 # hunk data before patching. Otherwise, preserve input
442 # line-endings.
442 # line-endings.
443 h = h.getnormalized()
443 h = h.getnormalized()
444
444
445 # fast case first, no offsets, no fuzz
445 # fast case first, no offsets, no fuzz
446 old = h.old()
446 old = h.old()
447 # patch starts counting at 1 unless we are adding the file
447 # patch starts counting at 1 unless we are adding the file
448 if h.starta == 0:
448 if h.starta == 0:
449 start = 0
449 start = 0
450 else:
450 else:
451 start = h.starta + self.offset - 1
451 start = h.starta + self.offset - 1
452 orig_start = start
452 orig_start = start
453 # if there's skew we want to emit the "(offset %d lines)" even
453 # if there's skew we want to emit the "(offset %d lines)" even
454 # when the hunk cleanly applies at start + skew, so skip the
454 # when the hunk cleanly applies at start + skew, so skip the
455 # fast case code
455 # fast case code
456 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
456 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
457 if h.rmfile():
457 if h.rmfile():
458 self.unlink(self.fname)
458 self.unlink(self.fname)
459 else:
459 else:
460 self.lines[start : start + h.lena] = h.new()
460 self.lines[start : start + h.lena] = h.new()
461 self.offset += h.lenb - h.lena
461 self.offset += h.lenb - h.lena
462 self.dirty = 1
462 self.dirty = 1
463 return 0
463 return 0
464
464
465 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
465 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
466 self.hashlines()
466 self.hashlines()
467 if h.hunk[-1][0] != ' ':
467 if h.hunk[-1][0] != ' ':
468 # if the hunk tried to put something at the bottom of the file
468 # if the hunk tried to put something at the bottom of the file
469 # override the start line and use eof here
469 # override the start line and use eof here
470 search_start = len(self.lines)
470 search_start = len(self.lines)
471 else:
471 else:
472 search_start = orig_start + self.skew
472 search_start = orig_start + self.skew
473
473
474 for fuzzlen in xrange(3):
474 for fuzzlen in xrange(3):
475 for toponly in [ True, False ]:
475 for toponly in [ True, False ]:
476 old = h.old(fuzzlen, toponly)
476 old = h.old(fuzzlen, toponly)
477
477
478 cand = self.findlines(old[0][1:], search_start)
478 cand = self.findlines(old[0][1:], search_start)
479 for l in cand:
479 for l in cand:
480 if diffhelpers.testhunk(old, self.lines, l) == 0:
480 if diffhelpers.testhunk(old, self.lines, l) == 0:
481 newlines = h.new(fuzzlen, toponly)
481 newlines = h.new(fuzzlen, toponly)
482 self.lines[l : l + len(old)] = newlines
482 self.lines[l : l + len(old)] = newlines
483 self.offset += len(newlines) - len(old)
483 self.offset += len(newlines) - len(old)
484 self.skew = l - orig_start
484 self.skew = l - orig_start
485 self.dirty = 1
485 self.dirty = 1
486 if fuzzlen:
486 if fuzzlen:
487 fuzzstr = "with fuzz %d " % fuzzlen
487 fuzzstr = "with fuzz %d " % fuzzlen
488 f = self.ui.warn
488 f = self.ui.warn
489 self.printfile(True)
489 self.printfile(True)
490 else:
490 else:
491 fuzzstr = ""
491 fuzzstr = ""
492 f = self.ui.note
492 f = self.ui.note
493 offset = l - orig_start - fuzzlen
493 offset = l - orig_start - fuzzlen
494 if offset == 1:
494 if offset == 1:
495 msg = _("Hunk #%d succeeded at %d %s"
495 msg = _("Hunk #%d succeeded at %d %s"
496 "(offset %d line).\n")
496 "(offset %d line).\n")
497 else:
497 else:
498 msg = _("Hunk #%d succeeded at %d %s"
498 msg = _("Hunk #%d succeeded at %d %s"
499 "(offset %d lines).\n")
499 "(offset %d lines).\n")
500 f(msg % (h.number, l+1, fuzzstr, offset))
500 f(msg % (h.number, l+1, fuzzstr, offset))
501 return fuzzlen
501 return fuzzlen
502 self.printfile(True)
502 self.printfile(True)
503 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
503 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
504 self.rej.append(horig)
504 self.rej.append(horig)
505 return -1
505 return -1
506
506
507 class hunk(object):
507 class hunk(object):
508 def __init__(self, desc, num, lr, context, create=False, remove=False):
508 def __init__(self, desc, num, lr, context, create=False, remove=False):
509 self.number = num
509 self.number = num
510 self.desc = desc
510 self.desc = desc
511 self.hunk = [ desc ]
511 self.hunk = [ desc ]
512 self.a = []
512 self.a = []
513 self.b = []
513 self.b = []
514 self.starta = self.lena = None
514 self.starta = self.lena = None
515 self.startb = self.lenb = None
515 self.startb = self.lenb = None
516 if lr is not None:
516 if lr is not None:
517 if context:
517 if context:
518 self.read_context_hunk(lr)
518 self.read_context_hunk(lr)
519 else:
519 else:
520 self.read_unified_hunk(lr)
520 self.read_unified_hunk(lr)
521 self.create = create
521 self.create = create
522 self.remove = remove and not create
522 self.remove = remove and not create
523
523
524 def getnormalized(self):
524 def getnormalized(self):
525 """Return a copy with line endings normalized to LF."""
525 """Return a copy with line endings normalized to LF."""
526
526
527 def normalize(lines):
527 def normalize(lines):
528 nlines = []
528 nlines = []
529 for line in lines:
529 for line in lines:
530 if line.endswith('\r\n'):
530 if line.endswith('\r\n'):
531 line = line[:-2] + '\n'
531 line = line[:-2] + '\n'
532 nlines.append(line)
532 nlines.append(line)
533 return nlines
533 return nlines
534
534
535 # Dummy object, it is rebuilt manually
535 # Dummy object, it is rebuilt manually
536 nh = hunk(self.desc, self.number, None, None, False, False)
536 nh = hunk(self.desc, self.number, None, None, False, False)
537 nh.number = self.number
537 nh.number = self.number
538 nh.desc = self.desc
538 nh.desc = self.desc
539 nh.a = normalize(self.a)
539 nh.a = normalize(self.a)
540 nh.b = normalize(self.b)
540 nh.b = normalize(self.b)
541 nh.starta = self.starta
541 nh.starta = self.starta
542 nh.startb = self.startb
542 nh.startb = self.startb
543 nh.lena = self.lena
543 nh.lena = self.lena
544 nh.lenb = self.lenb
544 nh.lenb = self.lenb
545 nh.create = self.create
545 nh.create = self.create
546 nh.remove = self.remove
546 nh.remove = self.remove
547 return nh
547 return nh
548
548
549 def read_unified_hunk(self, lr):
549 def read_unified_hunk(self, lr):
550 m = unidesc.match(self.desc)
550 m = unidesc.match(self.desc)
551 if not m:
551 if not m:
552 raise PatchError(_("bad hunk #%d") % self.number)
552 raise PatchError(_("bad hunk #%d") % self.number)
553 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
553 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
554 if self.lena is None:
554 if self.lena is None:
555 self.lena = 1
555 self.lena = 1
556 else:
556 else:
557 self.lena = int(self.lena)
557 self.lena = int(self.lena)
558 if self.lenb is None:
558 if self.lenb is None:
559 self.lenb = 1
559 self.lenb = 1
560 else:
560 else:
561 self.lenb = int(self.lenb)
561 self.lenb = int(self.lenb)
562 self.starta = int(self.starta)
562 self.starta = int(self.starta)
563 self.startb = int(self.startb)
563 self.startb = int(self.startb)
564 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
564 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
565 # if we hit eof before finishing out the hunk, the last line will
565 # if we hit eof before finishing out the hunk, the last line will
566 # be zero length. Lets try to fix it up.
566 # be zero length. Lets try to fix it up.
567 while len(self.hunk[-1]) == 0:
567 while len(self.hunk[-1]) == 0:
568 del self.hunk[-1]
568 del self.hunk[-1]
569 del self.a[-1]
569 del self.a[-1]
570 del self.b[-1]
570 del self.b[-1]
571 self.lena -= 1
571 self.lena -= 1
572 self.lenb -= 1
572 self.lenb -= 1
573
573
574 def read_context_hunk(self, lr):
574 def read_context_hunk(self, lr):
575 self.desc = lr.readline()
575 self.desc = lr.readline()
576 m = contextdesc.match(self.desc)
576 m = contextdesc.match(self.desc)
577 if not m:
577 if not m:
578 raise PatchError(_("bad hunk #%d") % self.number)
578 raise PatchError(_("bad hunk #%d") % self.number)
579 foo, self.starta, foo2, aend, foo3 = m.groups()
579 foo, self.starta, foo2, aend, foo3 = m.groups()
580 self.starta = int(self.starta)
580 self.starta = int(self.starta)
581 if aend is None:
581 if aend is None:
582 aend = self.starta
582 aend = self.starta
583 self.lena = int(aend) - self.starta
583 self.lena = int(aend) - self.starta
584 if self.starta:
584 if self.starta:
585 self.lena += 1
585 self.lena += 1
586 for x in xrange(self.lena):
586 for x in xrange(self.lena):
587 l = lr.readline()
587 l = lr.readline()
588 if l.startswith('---'):
588 if l.startswith('---'):
589 lr.push(l)
589 lr.push(l)
590 break
590 break
591 s = l[2:]
591 s = l[2:]
592 if l.startswith('- ') or l.startswith('! '):
592 if l.startswith('- ') or l.startswith('! '):
593 u = '-' + s
593 u = '-' + s
594 elif l.startswith(' '):
594 elif l.startswith(' '):
595 u = ' ' + s
595 u = ' ' + s
596 else:
596 else:
597 raise PatchError(_("bad hunk #%d old text line %d") %
597 raise PatchError(_("bad hunk #%d old text line %d") %
598 (self.number, x))
598 (self.number, x))
599 self.a.append(u)
599 self.a.append(u)
600 self.hunk.append(u)
600 self.hunk.append(u)
601
601
602 l = lr.readline()
602 l = lr.readline()
603 if l.startswith('\ '):
603 if l.startswith('\ '):
604 s = self.a[-1][:-1]
604 s = self.a[-1][:-1]
605 self.a[-1] = s
605 self.a[-1] = s
606 self.hunk[-1] = s
606 self.hunk[-1] = s
607 l = lr.readline()
607 l = lr.readline()
608 m = contextdesc.match(l)
608 m = contextdesc.match(l)
609 if not m:
609 if not m:
610 raise PatchError(_("bad hunk #%d") % self.number)
610 raise PatchError(_("bad hunk #%d") % self.number)
611 foo, self.startb, foo2, bend, foo3 = m.groups()
611 foo, self.startb, foo2, bend, foo3 = m.groups()
612 self.startb = int(self.startb)
612 self.startb = int(self.startb)
613 if bend is None:
613 if bend is None:
614 bend = self.startb
614 bend = self.startb
615 self.lenb = int(bend) - self.startb
615 self.lenb = int(bend) - self.startb
616 if self.startb:
616 if self.startb:
617 self.lenb += 1
617 self.lenb += 1
618 hunki = 1
618 hunki = 1
619 for x in xrange(self.lenb):
619 for x in xrange(self.lenb):
620 l = lr.readline()
620 l = lr.readline()
621 if l.startswith('\ '):
621 if l.startswith('\ '):
622 s = self.b[-1][:-1]
622 s = self.b[-1][:-1]
623 self.b[-1] = s
623 self.b[-1] = s
624 self.hunk[hunki-1] = s
624 self.hunk[hunki-1] = s
625 continue
625 continue
626 if not l:
626 if not l:
627 lr.push(l)
627 lr.push(l)
628 break
628 break
629 s = l[2:]
629 s = l[2:]
630 if l.startswith('+ ') or l.startswith('! '):
630 if l.startswith('+ ') or l.startswith('! '):
631 u = '+' + s
631 u = '+' + s
632 elif l.startswith(' '):
632 elif l.startswith(' '):
633 u = ' ' + s
633 u = ' ' + s
634 elif len(self.b) == 0:
634 elif len(self.b) == 0:
635 # this can happen when the hunk does not add any lines
635 # this can happen when the hunk does not add any lines
636 lr.push(l)
636 lr.push(l)
637 break
637 break
638 else:
638 else:
639 raise PatchError(_("bad hunk #%d old text line %d") %
639 raise PatchError(_("bad hunk #%d old text line %d") %
640 (self.number, x))
640 (self.number, x))
641 self.b.append(s)
641 self.b.append(s)
642 while True:
642 while True:
643 if hunki >= len(self.hunk):
643 if hunki >= len(self.hunk):
644 h = ""
644 h = ""
645 else:
645 else:
646 h = self.hunk[hunki]
646 h = self.hunk[hunki]
647 hunki += 1
647 hunki += 1
648 if h == u:
648 if h == u:
649 break
649 break
650 elif h.startswith('-'):
650 elif h.startswith('-'):
651 continue
651 continue
652 else:
652 else:
653 self.hunk.insert(hunki-1, u)
653 self.hunk.insert(hunki-1, u)
654 break
654 break
655
655
656 if not self.a:
656 if not self.a:
657 # this happens when lines were only added to the hunk
657 # this happens when lines were only added to the hunk
658 for x in self.hunk:
658 for x in self.hunk:
659 if x.startswith('-') or x.startswith(' '):
659 if x.startswith('-') or x.startswith(' '):
660 self.a.append(x)
660 self.a.append(x)
661 if not self.b:
661 if not self.b:
662 # this happens when lines were only deleted from the hunk
662 # this happens when lines were only deleted from the hunk
663 for x in self.hunk:
663 for x in self.hunk:
664 if x.startswith('+') or x.startswith(' '):
664 if x.startswith('+') or x.startswith(' '):
665 self.b.append(x[1:])
665 self.b.append(x[1:])
666 # @@ -start,len +start,len @@
666 # @@ -start,len +start,len @@
667 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
667 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
668 self.startb, self.lenb)
668 self.startb, self.lenb)
669 self.hunk[0] = self.desc
669 self.hunk[0] = self.desc
670
670
671 def fix_newline(self):
671 def fix_newline(self):
672 diffhelpers.fix_newline(self.hunk, self.a, self.b)
672 diffhelpers.fix_newline(self.hunk, self.a, self.b)
673
673
674 def complete(self):
674 def complete(self):
675 return len(self.a) == self.lena and len(self.b) == self.lenb
675 return len(self.a) == self.lena and len(self.b) == self.lenb
676
676
677 def createfile(self):
677 def createfile(self):
678 return self.starta == 0 and self.lena == 0 and self.create
678 return self.starta == 0 and self.lena == 0 and self.create
679
679
680 def rmfile(self):
680 def rmfile(self):
681 return self.startb == 0 and self.lenb == 0 and self.remove
681 return self.startb == 0 and self.lenb == 0 and self.remove
682
682
683 def fuzzit(self, l, fuzz, toponly):
683 def fuzzit(self, l, fuzz, toponly):
684 # this removes context lines from the top and bottom of list 'l'. It
684 # this removes context lines from the top and bottom of list 'l'. It
685 # checks the hunk to make sure only context lines are removed, and then
685 # checks the hunk to make sure only context lines are removed, and then
686 # returns a new shortened list of lines.
686 # returns a new shortened list of lines.
687 fuzz = min(fuzz, len(l)-1)
687 fuzz = min(fuzz, len(l)-1)
688 if fuzz:
688 if fuzz:
689 top = 0
689 top = 0
690 bot = 0
690 bot = 0
691 hlen = len(self.hunk)
691 hlen = len(self.hunk)
692 for x in xrange(hlen-1):
692 for x in xrange(hlen-1):
693 # the hunk starts with the @@ line, so use x+1
693 # the hunk starts with the @@ line, so use x+1
694 if self.hunk[x+1][0] == ' ':
694 if self.hunk[x+1][0] == ' ':
695 top += 1
695 top += 1
696 else:
696 else:
697 break
697 break
698 if not toponly:
698 if not toponly:
699 for x in xrange(hlen-1):
699 for x in xrange(hlen-1):
700 if self.hunk[hlen-bot-1][0] == ' ':
700 if self.hunk[hlen-bot-1][0] == ' ':
701 bot += 1
701 bot += 1
702 else:
702 else:
703 break
703 break
704
704
705 # top and bot now count context in the hunk
705 # top and bot now count context in the hunk
706 # adjust them if either one is short
706 # adjust them if either one is short
707 context = max(top, bot, 3)
707 context = max(top, bot, 3)
708 if bot < context:
708 if bot < context:
709 bot = max(0, fuzz - (context - bot))
709 bot = max(0, fuzz - (context - bot))
710 else:
710 else:
711 bot = min(fuzz, bot)
711 bot = min(fuzz, bot)
712 if top < context:
712 if top < context:
713 top = max(0, fuzz - (context - top))
713 top = max(0, fuzz - (context - top))
714 else:
714 else:
715 top = min(fuzz, top)
715 top = min(fuzz, top)
716
716
717 return l[top:len(l)-bot]
717 return l[top:len(l)-bot]
718 return l
718 return l
719
719
720 def old(self, fuzz=0, toponly=False):
720 def old(self, fuzz=0, toponly=False):
721 return self.fuzzit(self.a, fuzz, toponly)
721 return self.fuzzit(self.a, fuzz, toponly)
722
722
723 def new(self, fuzz=0, toponly=False):
723 def new(self, fuzz=0, toponly=False):
724 return self.fuzzit(self.b, fuzz, toponly)
724 return self.fuzzit(self.b, fuzz, toponly)
725
725
726 class binhunk:
726 class binhunk:
727 'A binary patch file. Only understands literals so far.'
727 'A binary patch file. Only understands literals so far.'
728 def __init__(self, gitpatch):
728 def __init__(self, gitpatch):
729 self.gitpatch = gitpatch
729 self.gitpatch = gitpatch
730 self.text = None
730 self.text = None
731 self.hunk = ['GIT binary patch\n']
731 self.hunk = ['GIT binary patch\n']
732
732
733 def createfile(self):
733 def createfile(self):
734 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
734 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
735
735
736 def rmfile(self):
736 def rmfile(self):
737 return self.gitpatch.op == 'DELETE'
737 return self.gitpatch.op == 'DELETE'
738
738
739 def complete(self):
739 def complete(self):
740 return self.text is not None
740 return self.text is not None
741
741
742 def new(self):
742 def new(self):
743 return [self.text]
743 return [self.text]
744
744
745 def extract(self, lr):
745 def extract(self, lr):
746 line = lr.readline()
746 line = lr.readline()
747 self.hunk.append(line)
747 self.hunk.append(line)
748 while line and not line.startswith('literal '):
748 while line and not line.startswith('literal '):
749 line = lr.readline()
749 line = lr.readline()
750 self.hunk.append(line)
750 self.hunk.append(line)
751 if not line:
751 if not line:
752 raise PatchError(_('could not extract binary patch'))
752 raise PatchError(_('could not extract binary patch'))
753 size = int(line[8:].rstrip())
753 size = int(line[8:].rstrip())
754 dec = []
754 dec = []
755 line = lr.readline()
755 line = lr.readline()
756 self.hunk.append(line)
756 self.hunk.append(line)
757 while len(line) > 1:
757 while len(line) > 1:
758 l = line[0]
758 l = line[0]
759 if l <= 'Z' and l >= 'A':
759 if l <= 'Z' and l >= 'A':
760 l = ord(l) - ord('A') + 1
760 l = ord(l) - ord('A') + 1
761 else:
761 else:
762 l = ord(l) - ord('a') + 27
762 l = ord(l) - ord('a') + 27
763 dec.append(base85.b85decode(line[1:-1])[:l])
763 dec.append(base85.b85decode(line[1:-1])[:l])
764 line = lr.readline()
764 line = lr.readline()
765 self.hunk.append(line)
765 self.hunk.append(line)
766 text = zlib.decompress(''.join(dec))
766 text = zlib.decompress(''.join(dec))
767 if len(text) != size:
767 if len(text) != size:
768 raise PatchError(_('binary patch is %d bytes, not %d') %
768 raise PatchError(_('binary patch is %d bytes, not %d') %
769 len(text), size)
769 len(text), size)
770 self.text = text
770 self.text = text
771
771
772 def parsefilename(str):
772 def parsefilename(str):
773 # --- filename \t|space stuff
773 # --- filename \t|space stuff
774 s = str[4:].rstrip('\r\n')
774 s = str[4:].rstrip('\r\n')
775 i = s.find('\t')
775 i = s.find('\t')
776 if i < 0:
776 if i < 0:
777 i = s.find(' ')
777 i = s.find(' ')
778 if i < 0:
778 if i < 0:
779 return s
779 return s
780 return s[:i]
780 return s[:i]
781
781
782 def selectfile(afile_orig, bfile_orig, hunk, strip):
782 def selectfile(afile_orig, bfile_orig, hunk, strip):
783 def pathstrip(path, count=1):
783 def pathstrip(path, count=1):
784 pathlen = len(path)
784 pathlen = len(path)
785 i = 0
785 i = 0
786 if count == 0:
786 if count == 0:
787 return '', path.rstrip()
787 return '', path.rstrip()
788 while count > 0:
788 while count > 0:
789 i = path.find('/', i)
789 i = path.find('/', i)
790 if i == -1:
790 if i == -1:
791 raise PatchError(_("unable to strip away %d dirs from %s") %
791 raise PatchError(_("unable to strip away %d dirs from %s") %
792 (count, path))
792 (count, path))
793 i += 1
793 i += 1
794 # consume '//' in the path
794 # consume '//' in the path
795 while i < pathlen - 1 and path[i] == '/':
795 while i < pathlen - 1 and path[i] == '/':
796 i += 1
796 i += 1
797 count -= 1
797 count -= 1
798 return path[:i].lstrip(), path[i:].rstrip()
798 return path[:i].lstrip(), path[i:].rstrip()
799
799
800 nulla = afile_orig == "/dev/null"
800 nulla = afile_orig == "/dev/null"
801 nullb = bfile_orig == "/dev/null"
801 nullb = bfile_orig == "/dev/null"
802 abase, afile = pathstrip(afile_orig, strip)
802 abase, afile = pathstrip(afile_orig, strip)
803 gooda = not nulla and util.lexists(afile)
803 gooda = not nulla and util.lexists(afile)
804 bbase, bfile = pathstrip(bfile_orig, strip)
804 bbase, bfile = pathstrip(bfile_orig, strip)
805 if afile == bfile:
805 if afile == bfile:
806 goodb = gooda
806 goodb = gooda
807 else:
807 else:
808 goodb = not nullb and os.path.exists(bfile)
808 goodb = not nullb and os.path.exists(bfile)
809 createfunc = hunk.createfile
809 createfunc = hunk.createfile
810 missing = not goodb and not gooda and not createfunc()
810 missing = not goodb and not gooda and not createfunc()
811
811
812 # some diff programs apparently produce create patches where the
812 # some diff programs apparently produce create patches where the
813 # afile is not /dev/null, but rather the same name as the bfile
813 # afile is not /dev/null, but rather the same name as the bfile
814 if missing and afile == bfile:
814 if missing and afile == bfile:
815 # this isn't very pretty
815 # this isn't very pretty
816 hunk.create = True
816 hunk.create = True
817 if createfunc():
817 if createfunc():
818 missing = False
818 missing = False
819 else:
819 else:
820 hunk.create = False
820 hunk.create = False
821
821
822 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
822 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
823 # diff is between a file and its backup. In this case, the original
823 # diff is between a file and its backup. In this case, the original
824 # file should be patched (see original mpatch code).
824 # file should be patched (see original mpatch code).
825 isbackup = (abase == bbase and bfile.startswith(afile))
825 isbackup = (abase == bbase and bfile.startswith(afile))
826 fname = None
826 fname = None
827 if not missing:
827 if not missing:
828 if gooda and goodb:
828 if gooda and goodb:
829 fname = isbackup and afile or bfile
829 fname = isbackup and afile or bfile
830 elif gooda:
830 elif gooda:
831 fname = afile
831 fname = afile
832
832
833 if not fname:
833 if not fname:
834 if not nullb:
834 if not nullb:
835 fname = isbackup and afile or bfile
835 fname = isbackup and afile or bfile
836 elif not nulla:
836 elif not nulla:
837 fname = afile
837 fname = afile
838 else:
838 else:
839 raise PatchError(_("undefined source and destination files"))
839 raise PatchError(_("undefined source and destination files"))
840
840
841 return fname, missing
841 return fname, missing
842
842
843 def scangitpatch(lr, firstline):
843 def scangitpatch(lr, firstline):
844 """
844 """
845 Git patches can emit:
845 Git patches can emit:
846 - rename a to b
846 - rename a to b
847 - change b
847 - change b
848 - copy a to c
848 - copy a to c
849 - change c
849 - change c
850
850
851 We cannot apply this sequence as-is, the renamed 'a' could not be
851 We cannot apply this sequence as-is, the renamed 'a' could not be
852 found for it would have been renamed already. And we cannot copy
852 found for it would have been renamed already. And we cannot copy
853 from 'b' instead because 'b' would have been changed already. So
853 from 'b' instead because 'b' would have been changed already. So
854 we scan the git patch for copy and rename commands so we can
854 we scan the git patch for copy and rename commands so we can
855 perform the copies ahead of time.
855 perform the copies ahead of time.
856 """
856 """
857 pos = 0
857 pos = 0
858 try:
858 try:
859 pos = lr.fp.tell()
859 pos = lr.fp.tell()
860 fp = lr.fp
860 fp = lr.fp
861 except IOError:
861 except IOError:
862 fp = cStringIO.StringIO(lr.fp.read())
862 fp = cStringIO.StringIO(lr.fp.read())
863 gitlr = linereader(fp, lr.textmode)
863 gitlr = linereader(fp, lr.textmode)
864 gitlr.push(firstline)
864 gitlr.push(firstline)
865 (dopatch, gitpatches) = readgitpatch(gitlr)
865 (dopatch, gitpatches) = readgitpatch(gitlr)
866 fp.seek(pos)
866 fp.seek(pos)
867 return dopatch, gitpatches
867 return dopatch, gitpatches
868
868
869 def iterhunks(ui, fp, sourcefile=None):
869 def iterhunks(ui, fp, sourcefile=None):
870 """Read a patch and yield the following events:
870 """Read a patch and yield the following events:
871 - ("file", afile, bfile, firsthunk): select a new target file.
871 - ("file", afile, bfile, firsthunk): select a new target file.
872 - ("hunk", hunk): a new hunk is ready to be applied, follows a
872 - ("hunk", hunk): a new hunk is ready to be applied, follows a
873 "file" event.
873 "file" event.
874 - ("git", gitchanges): current diff is in git format, gitchanges
874 - ("git", gitchanges): current diff is in git format, gitchanges
875 maps filenames to gitpatch records. Unique event.
875 maps filenames to gitpatch records. Unique event.
876 """
876 """
877 changed = {}
877 changed = {}
878 current_hunk = None
878 current_hunk = None
879 afile = ""
879 afile = ""
880 bfile = ""
880 bfile = ""
881 state = None
881 state = None
882 hunknum = 0
882 hunknum = 0
883 emitfile = False
883 emitfile = False
884 git = False
884 git = False
885
885
886 # our states
886 # our states
887 BFILE = 1
887 BFILE = 1
888 context = None
888 context = None
889 lr = linereader(fp)
889 lr = linereader(fp)
890 dopatch = True
890 dopatch = True
891 # gitworkdone is True if a git operation (copy, rename, ...) was
891 # gitworkdone is True if a git operation (copy, rename, ...) was
892 # performed already for the current file. Useful when the file
892 # performed already for the current file. Useful when the file
893 # section may have no hunk.
893 # section may have no hunk.
894 gitworkdone = False
894 gitworkdone = False
895
895
896 while True:
896 while True:
897 newfile = False
897 newfile = False
898 x = lr.readline()
898 x = lr.readline()
899 if not x:
899 if not x:
900 break
900 break
901 if current_hunk:
901 if current_hunk:
902 if x.startswith('\ '):
902 if x.startswith('\ '):
903 current_hunk.fix_newline()
903 current_hunk.fix_newline()
904 yield 'hunk', current_hunk
904 yield 'hunk', current_hunk
905 current_hunk = None
905 current_hunk = None
906 gitworkdone = False
906 gitworkdone = False
907 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
907 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
908 ((context is not False) and x.startswith('***************')))):
908 ((context is not False) and x.startswith('***************')))):
909 try:
909 try:
910 if context is None and x.startswith('***************'):
910 if context is None and x.startswith('***************'):
911 context = True
911 context = True
912 gpatch = changed.get(bfile)
912 gpatch = changed.get(bfile)
913 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
913 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
914 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
914 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
915 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
915 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
916 except PatchError, err:
916 except PatchError, err:
917 ui.debug(err)
917 ui.debug(err)
918 current_hunk = None
918 current_hunk = None
919 continue
919 continue
920 hunknum += 1
920 hunknum += 1
921 if emitfile:
921 if emitfile:
922 emitfile = False
922 emitfile = False
923 yield 'file', (afile, bfile, current_hunk)
923 yield 'file', (afile, bfile, current_hunk)
924 elif state == BFILE and x.startswith('GIT binary patch'):
924 elif state == BFILE and x.startswith('GIT binary patch'):
925 current_hunk = binhunk(changed[bfile])
925 current_hunk = binhunk(changed[bfile])
926 hunknum += 1
926 hunknum += 1
927 if emitfile:
927 if emitfile:
928 emitfile = False
928 emitfile = False
929 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
929 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
930 current_hunk.extract(lr)
930 current_hunk.extract(lr)
931 elif x.startswith('diff --git'):
931 elif x.startswith('diff --git'):
932 # check for git diff, scanning the whole patch file if needed
932 # check for git diff, scanning the whole patch file if needed
933 m = gitre.match(x)
933 m = gitre.match(x)
934 if m:
934 if m:
935 afile, bfile = m.group(1, 2)
935 afile, bfile = m.group(1, 2)
936 if not git:
936 if not git:
937 git = True
937 git = True
938 dopatch, gitpatches = scangitpatch(lr, x)
938 dopatch, gitpatches = scangitpatch(lr, x)
939 yield 'git', gitpatches
939 yield 'git', gitpatches
940 for gp in gitpatches:
940 for gp in gitpatches:
941 changed[gp.path] = gp
941 changed[gp.path] = gp
942 # else error?
942 # else error?
943 # copy/rename + modify should modify target, not source
943 # copy/rename + modify should modify target, not source
944 gp = changed.get(bfile)
944 gp = changed.get(bfile)
945 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
945 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
946 afile = bfile
946 afile = bfile
947 gitworkdone = True
947 gitworkdone = True
948 newfile = True
948 newfile = True
949 elif x.startswith('---'):
949 elif x.startswith('---'):
950 # check for a unified diff
950 # check for a unified diff
951 l2 = lr.readline()
951 l2 = lr.readline()
952 if not l2.startswith('+++'):
952 if not l2.startswith('+++'):
953 lr.push(l2)
953 lr.push(l2)
954 continue
954 continue
955 newfile = True
955 newfile = True
956 context = False
956 context = False
957 afile = parsefilename(x)
957 afile = parsefilename(x)
958 bfile = parsefilename(l2)
958 bfile = parsefilename(l2)
959 elif x.startswith('***'):
959 elif x.startswith('***'):
960 # check for a context diff
960 # check for a context diff
961 l2 = lr.readline()
961 l2 = lr.readline()
962 if not l2.startswith('---'):
962 if not l2.startswith('---'):
963 lr.push(l2)
963 lr.push(l2)
964 continue
964 continue
965 l3 = lr.readline()
965 l3 = lr.readline()
966 lr.push(l3)
966 lr.push(l3)
967 if not l3.startswith("***************"):
967 if not l3.startswith("***************"):
968 lr.push(l2)
968 lr.push(l2)
969 continue
969 continue
970 newfile = True
970 newfile = True
971 context = True
971 context = True
972 afile = parsefilename(x)
972 afile = parsefilename(x)
973 bfile = parsefilename(l2)
973 bfile = parsefilename(l2)
974
974
975 if newfile:
975 if newfile:
976 emitfile = True
976 emitfile = True
977 state = BFILE
977 state = BFILE
978 hunknum = 0
978 hunknum = 0
979 if current_hunk:
979 if current_hunk:
980 if current_hunk.complete():
980 if current_hunk.complete():
981 yield 'hunk', current_hunk
981 yield 'hunk', current_hunk
982 else:
982 else:
983 raise PatchError(_("malformed patch %s %s") % (afile,
983 raise PatchError(_("malformed patch %s %s") % (afile,
984 current_hunk.desc))
984 current_hunk.desc))
985
985
986 if hunknum == 0 and dopatch and not gitworkdone:
986 if hunknum == 0 and dopatch and not gitworkdone:
987 raise NoHunks
987 raise NoHunks
988
988
989 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
989 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
990 """
990 """
991 Reads a patch from fp and tries to apply it.
991 Reads a patch from fp and tries to apply it.
992
992
993 The dict 'changed' is filled in with all of the filenames changed
993 The dict 'changed' is filled in with all of the filenames changed
994 by the patch. Returns 0 for a clean patch, -1 if any rejects were
994 by the patch. Returns 0 for a clean patch, -1 if any rejects were
995 found and 1 if there was any fuzz.
995 found and 1 if there was any fuzz.
996
996
997 If 'eolmode' is 'strict', the patch content and patched file are
997 If 'eolmode' is 'strict', the patch content and patched file are
998 read in binary mode. Otherwise, line endings are ignored when
998 read in binary mode. Otherwise, line endings are ignored when
999 patching then normalized according to 'eolmode'.
999 patching then normalized according to 'eolmode'.
1000 """
1000 """
1001 rejects = 0
1001 rejects = 0
1002 err = 0
1002 err = 0
1003 current_file = None
1003 current_file = None
1004 gitpatches = None
1004 gitpatches = None
1005 opener = util.opener(os.getcwd())
1005 opener = util.opener(os.getcwd())
1006
1006
1007 def closefile():
1007 def closefile():
1008 if not current_file:
1008 if not current_file:
1009 return 0
1009 return 0
1010 current_file.close()
1010 current_file.close()
1011 return len(current_file.rej)
1011 return len(current_file.rej)
1012
1012
1013 for state, values in iterhunks(ui, fp, sourcefile):
1013 for state, values in iterhunks(ui, fp, sourcefile):
1014 if state == 'hunk':
1014 if state == 'hunk':
1015 if not current_file:
1015 if not current_file:
1016 continue
1016 continue
1017 current_hunk = values
1017 current_hunk = values
1018 ret = current_file.apply(current_hunk)
1018 ret = current_file.apply(current_hunk)
1019 if ret >= 0:
1019 if ret >= 0:
1020 changed.setdefault(current_file.fname, None)
1020 changed.setdefault(current_file.fname, None)
1021 if ret > 0:
1021 if ret > 0:
1022 err = 1
1022 err = 1
1023 elif state == 'file':
1023 elif state == 'file':
1024 rejects += closefile()
1024 rejects += closefile()
1025 afile, bfile, first_hunk = values
1025 afile, bfile, first_hunk = values
1026 try:
1026 try:
1027 if sourcefile:
1027 if sourcefile:
1028 current_file = patchfile(ui, sourcefile, opener, eolmode=eolmode)
1028 current_file = patchfile(ui, sourcefile, opener, eolmode=eolmode)
1029 else:
1029 else:
1030 current_file, missing = selectfile(afile, bfile, first_hunk,
1030 current_file, missing = selectfile(afile, bfile, first_hunk,
1031 strip)
1031 strip)
1032 current_file = patchfile(ui, current_file, opener, missing, eolmode)
1032 current_file = patchfile(ui, current_file, opener, missing, eolmode)
1033 except PatchError, err:
1033 except PatchError, err:
1034 ui.warn(str(err) + '\n')
1034 ui.warn(str(err) + '\n')
1035 current_file, current_hunk = None, None
1035 current_file, current_hunk = None, None
1036 rejects += 1
1036 rejects += 1
1037 continue
1037 continue
1038 elif state == 'git':
1038 elif state == 'git':
1039 gitpatches = values
1039 gitpatches = values
1040 cwd = os.getcwd()
1040 cwd = os.getcwd()
1041 for gp in gitpatches:
1041 for gp in gitpatches:
1042 if gp.op in ('COPY', 'RENAME'):
1042 if gp.op in ('COPY', 'RENAME'):
1043 copyfile(gp.oldpath, gp.path, cwd)
1043 copyfile(gp.oldpath, gp.path, cwd)
1044 changed[gp.path] = gp
1044 changed[gp.path] = gp
1045 else:
1045 else:
1046 raise util.Abort(_('unsupported parser state: %s') % state)
1046 raise util.Abort(_('unsupported parser state: %s') % state)
1047
1047
1048 rejects += closefile()
1048 rejects += closefile()
1049
1049
1050 if rejects:
1050 if rejects:
1051 return -1
1051 return -1
1052 return err
1052 return err
1053
1053
1054 def diffopts(ui, opts=None, untrusted=False):
1054 def diffopts(ui, opts=None, untrusted=False):
1055 def get(key, name=None, getter=ui.configbool):
1055 def get(key, name=None, getter=ui.configbool):
1056 return ((opts and opts.get(key)) or
1056 return ((opts and opts.get(key)) or
1057 getter('diff', name or key, None, untrusted=untrusted))
1057 getter('diff', name or key, None, untrusted=untrusted))
1058 return mdiff.diffopts(
1058 return mdiff.diffopts(
1059 text=opts and opts.get('text'),
1059 text=opts and opts.get('text'),
1060 git=get('git'),
1060 git=get('git'),
1061 nodates=get('nodates'),
1061 nodates=get('nodates'),
1062 showfunc=get('show_function', 'showfunc'),
1062 showfunc=get('show_function', 'showfunc'),
1063 ignorews=get('ignore_all_space', 'ignorews'),
1063 ignorews=get('ignore_all_space', 'ignorews'),
1064 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1064 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1065 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1065 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1066 context=get('unified', getter=ui.config))
1066 context=get('unified', getter=ui.config))
1067
1067
1068 def updatedir(ui, repo, patches, similarity=0):
1068 def updatedir(ui, repo, patches, similarity=0):
1069 '''Update dirstate after patch application according to metadata'''
1069 '''Update dirstate after patch application according to metadata'''
1070 if not patches:
1070 if not patches:
1071 return
1071 return
1072 copies = []
1072 copies = []
1073 removes = set()
1073 removes = set()
1074 cfiles = patches.keys()
1074 cfiles = patches.keys()
1075 cwd = repo.getcwd()
1075 cwd = repo.getcwd()
1076 if cwd:
1076 if cwd:
1077 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1077 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1078 for f in patches:
1078 for f in patches:
1079 gp = patches[f]
1079 gp = patches[f]
1080 if not gp:
1080 if not gp:
1081 continue
1081 continue
1082 if gp.op == 'RENAME':
1082 if gp.op == 'RENAME':
1083 copies.append((gp.oldpath, gp.path))
1083 copies.append((gp.oldpath, gp.path))
1084 removes.add(gp.oldpath)
1084 removes.add(gp.oldpath)
1085 elif gp.op == 'COPY':
1085 elif gp.op == 'COPY':
1086 copies.append((gp.oldpath, gp.path))
1086 copies.append((gp.oldpath, gp.path))
1087 elif gp.op == 'DELETE':
1087 elif gp.op == 'DELETE':
1088 removes.add(gp.path)
1088 removes.add(gp.path)
1089 for src, dst in copies:
1089 for src, dst in copies:
1090 repo.copy(src, dst)
1090 repo.copy(src, dst)
1091 if (not similarity) and removes:
1091 if (not similarity) and removes:
1092 repo.remove(sorted(removes), True)
1092 repo.remove(sorted(removes), True)
1093 for f in patches:
1093 for f in patches:
1094 gp = patches[f]
1094 gp = patches[f]
1095 if gp and gp.mode:
1095 if gp and gp.mode:
1096 islink, isexec = gp.mode
1096 islink, isexec = gp.mode
1097 dst = repo.wjoin(gp.path)
1097 dst = repo.wjoin(gp.path)
1098 # patch won't create empty files
1098 # patch won't create empty files
1099 if gp.op == 'ADD' and not os.path.exists(dst):
1099 if gp.op == 'ADD' and not os.path.exists(dst):
1100 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1100 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1101 repo.wwrite(gp.path, '', flags)
1101 repo.wwrite(gp.path, '', flags)
1102 elif gp.op != 'DELETE':
1102 elif gp.op != 'DELETE':
1103 util.set_flags(dst, islink, isexec)
1103 util.set_flags(dst, islink, isexec)
1104 cmdutil.addremove(repo, cfiles, similarity=similarity)
1104 cmdutil.addremove(repo, cfiles, similarity=similarity)
1105 files = patches.keys()
1105 files = patches.keys()
1106 files.extend([r for r in removes if r not in files])
1106 files.extend([r for r in removes if r not in files])
1107 return sorted(files)
1107 return sorted(files)
1108
1108
1109 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1109 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1110 """use <patcher> to apply <patchname> to the working directory.
1110 """use <patcher> to apply <patchname> to the working directory.
1111 returns whether patch was applied with fuzz factor."""
1111 returns whether patch was applied with fuzz factor."""
1112
1112
1113 fuzz = False
1113 fuzz = False
1114 if cwd:
1114 if cwd:
1115 args.append('-d %s' % util.shellquote(cwd))
1115 args.append('-d %s' % util.shellquote(cwd))
1116 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1116 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1117 util.shellquote(patchname)))
1117 util.shellquote(patchname)))
1118
1118
1119 for line in fp:
1119 for line in fp:
1120 line = line.rstrip()
1120 line = line.rstrip()
1121 ui.note(line + '\n')
1121 ui.note(line + '\n')
1122 if line.startswith('patching file '):
1122 if line.startswith('patching file '):
1123 pf = util.parse_patch_output(line)
1123 pf = util.parse_patch_output(line)
1124 printed_file = False
1124 printed_file = False
1125 files.setdefault(pf, None)
1125 files.setdefault(pf, None)
1126 elif line.find('with fuzz') >= 0:
1126 elif line.find('with fuzz') >= 0:
1127 fuzz = True
1127 fuzz = True
1128 if not printed_file:
1128 if not printed_file:
1129 ui.warn(pf + '\n')
1129 ui.warn(pf + '\n')
1130 printed_file = True
1130 printed_file = True
1131 ui.warn(line + '\n')
1131 ui.warn(line + '\n')
1132 elif line.find('saving rejects to file') >= 0:
1132 elif line.find('saving rejects to file') >= 0:
1133 ui.warn(line + '\n')
1133 ui.warn(line + '\n')
1134 elif line.find('FAILED') >= 0:
1134 elif line.find('FAILED') >= 0:
1135 if not printed_file:
1135 if not printed_file:
1136 ui.warn(pf + '\n')
1136 ui.warn(pf + '\n')
1137 printed_file = True
1137 printed_file = True
1138 ui.warn(line + '\n')
1138 ui.warn(line + '\n')
1139 code = fp.close()
1139 code = fp.close()
1140 if code:
1140 if code:
1141 raise PatchError(_("patch command failed: %s") %
1141 raise PatchError(_("patch command failed: %s") %
1142 util.explain_exit(code)[0])
1142 util.explain_exit(code)[0])
1143 return fuzz
1143 return fuzz
1144
1144
1145 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1145 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1146 """use builtin patch to apply <patchobj> to the working directory.
1146 """use builtin patch to apply <patchobj> to the working directory.
1147 returns whether patch was applied with fuzz factor."""
1147 returns whether patch was applied with fuzz factor."""
1148
1148
1149 if files is None:
1149 if files is None:
1150 files = {}
1150 files = {}
1151 if eolmode is None:
1151 if eolmode is None:
1152 eolmode = ui.config('patch', 'eol', 'strict')
1152 eolmode = ui.config('patch', 'eol', 'strict')
1153 if eolmode.lower() not in eolmodes:
1153 if eolmode.lower() not in eolmodes:
1154 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1154 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1155 eolmode = eolmode.lower()
1155 eolmode = eolmode.lower()
1156
1156
1157 try:
1157 try:
1158 fp = open(patchobj, 'rb')
1158 fp = open(patchobj, 'rb')
1159 except TypeError:
1159 except TypeError:
1160 fp = patchobj
1160 fp = patchobj
1161 if cwd:
1161 if cwd:
1162 curdir = os.getcwd()
1162 curdir = os.getcwd()
1163 os.chdir(cwd)
1163 os.chdir(cwd)
1164 try:
1164 try:
1165 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1165 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1166 finally:
1166 finally:
1167 if cwd:
1167 if cwd:
1168 os.chdir(curdir)
1168 os.chdir(curdir)
1169 if ret < 0:
1169 if ret < 0:
1170 raise PatchError
1170 raise PatchError
1171 return ret > 0
1171 return ret > 0
1172
1172
1173 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1173 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1174 """Apply <patchname> to the working directory.
1174 """Apply <patchname> to the working directory.
1175
1175
1176 'eolmode' specifies how end of lines should be handled. It can be:
1176 'eolmode' specifies how end of lines should be handled. It can be:
1177 - 'strict': inputs are read in binary mode, EOLs are preserved
1177 - 'strict': inputs are read in binary mode, EOLs are preserved
1178 - 'crlf': EOLs are ignored when patching and reset to CRLF
1178 - 'crlf': EOLs are ignored when patching and reset to CRLF
1179 - 'lf': EOLs are ignored when patching and reset to LF
1179 - 'lf': EOLs are ignored when patching and reset to LF
1180 - None: get it from user settings, default to 'strict'
1180 - None: get it from user settings, default to 'strict'
1181 'eolmode' is ignored when using an external patcher program.
1181 'eolmode' is ignored when using an external patcher program.
1182
1182
1183 Returns whether patch was applied with fuzz factor.
1183 Returns whether patch was applied with fuzz factor.
1184 """
1184 """
1185 patcher = ui.config('ui', 'patch')
1185 patcher = ui.config('ui', 'patch')
1186 args = []
1186 args = []
1187 if files is None:
1187 if files is None:
1188 files = {}
1188 files = {}
1189 try:
1189 try:
1190 if patcher:
1190 if patcher:
1191 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1191 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1192 files)
1192 files)
1193 else:
1193 else:
1194 try:
1194 try:
1195 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1195 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1196 except NoHunks:
1196 except NoHunks:
1197 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch'
1197 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch'
1198 ui.debug('no valid hunks found; trying with %r instead\n' %
1198 ui.debug('no valid hunks found; trying with %r instead\n' %
1199 patcher)
1199 patcher)
1200 if util.needbinarypatch():
1200 if util.needbinarypatch():
1201 args.append('--binary')
1201 args.append('--binary')
1202 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1202 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1203 files)
1203 files)
1204 except PatchError, err:
1204 except PatchError, err:
1205 s = str(err)
1205 s = str(err)
1206 if s:
1206 if s:
1207 raise util.Abort(s)
1207 raise util.Abort(s)
1208 else:
1208 else:
1209 raise util.Abort(_('patch failed to apply'))
1209 raise util.Abort(_('patch failed to apply'))
1210
1210
1211 def b85diff(to, tn):
1211 def b85diff(to, tn):
1212 '''print base85-encoded binary diff'''
1212 '''print base85-encoded binary diff'''
1213 def gitindex(text):
1213 def gitindex(text):
1214 if not text:
1214 if not text:
1215 return '0' * 40
1215 return '0' * 40
1216 l = len(text)
1216 l = len(text)
1217 s = util.sha1('blob %d\0' % l)
1217 s = util.sha1('blob %d\0' % l)
1218 s.update(text)
1218 s.update(text)
1219 return s.hexdigest()
1219 return s.hexdigest()
1220
1220
1221 def fmtline(line):
1221 def fmtline(line):
1222 l = len(line)
1222 l = len(line)
1223 if l <= 26:
1223 if l <= 26:
1224 l = chr(ord('A') + l - 1)
1224 l = chr(ord('A') + l - 1)
1225 else:
1225 else:
1226 l = chr(l - 26 + ord('a') - 1)
1226 l = chr(l - 26 + ord('a') - 1)
1227 return '%c%s\n' % (l, base85.b85encode(line, True))
1227 return '%c%s\n' % (l, base85.b85encode(line, True))
1228
1228
1229 def chunk(text, csize=52):
1229 def chunk(text, csize=52):
1230 l = len(text)
1230 l = len(text)
1231 i = 0
1231 i = 0
1232 while i < l:
1232 while i < l:
1233 yield text[i:i+csize]
1233 yield text[i:i+csize]
1234 i += csize
1234 i += csize
1235
1235
1236 tohash = gitindex(to)
1236 tohash = gitindex(to)
1237 tnhash = gitindex(tn)
1237 tnhash = gitindex(tn)
1238 if tohash == tnhash:
1238 if tohash == tnhash:
1239 return ""
1239 return ""
1240
1240
1241 # TODO: deltas
1241 # TODO: deltas
1242 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1242 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1243 (tohash, tnhash, len(tn))]
1243 (tohash, tnhash, len(tn))]
1244 for l in chunk(zlib.compress(tn)):
1244 for l in chunk(zlib.compress(tn)):
1245 ret.append(fmtline(l))
1245 ret.append(fmtline(l))
1246 ret.append('\n')
1246 ret.append('\n')
1247 return ''.join(ret)
1247 return ''.join(ret)
1248
1248
1249 def _addmodehdr(header, omode, nmode):
1249 class GitDiffRequired(Exception):
1250 if omode != nmode:
1250 pass
1251 header.append('old mode %s\n' % omode)
1252 header.append('new mode %s\n' % nmode)
1253
1251
1254 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None):
1252 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1253 losedatafn=None):
1255 '''yields diff of changes to files between two nodes, or node and
1254 '''yields diff of changes to files between two nodes, or node and
1256 working directory.
1255 working directory.
1257
1256
1258 if node1 is None, use first dirstate parent instead.
1257 if node1 is None, use first dirstate parent instead.
1259 if node2 is None, compare node1 with working directory.'''
1258 if node2 is None, compare node1 with working directory.
1259
1260 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1261 every time some change cannot be represented with the current
1262 patch format. Return False to upgrade to git patch format, True to
1263 accept the loss or raise an exception to abort the diff. It is
1264 called with the name of current file being diffed as 'fn'. If set
1265 to None, patches will always be upgraded to git format when
1266 necessary.
1267 '''
1260
1268
1261 if opts is None:
1269 if opts is None:
1262 opts = mdiff.defaultopts
1270 opts = mdiff.defaultopts
1263
1271
1264 if not node1 and not node2:
1272 if not node1 and not node2:
1265 node1 = repo.dirstate.parents()[0]
1273 node1 = repo.dirstate.parents()[0]
1266
1274
1267 def lrugetfilectx():
1275 def lrugetfilectx():
1268 cache = {}
1276 cache = {}
1269 order = []
1277 order = []
1270 def getfilectx(f, ctx):
1278 def getfilectx(f, ctx):
1271 fctx = ctx.filectx(f, filelog=cache.get(f))
1279 fctx = ctx.filectx(f, filelog=cache.get(f))
1272 if f not in cache:
1280 if f not in cache:
1273 if len(cache) > 20:
1281 if len(cache) > 20:
1274 del cache[order.pop(0)]
1282 del cache[order.pop(0)]
1275 cache[f] = fctx.filelog()
1283 cache[f] = fctx.filelog()
1276 else:
1284 else:
1277 order.remove(f)
1285 order.remove(f)
1278 order.append(f)
1286 order.append(f)
1279 return fctx
1287 return fctx
1280 return getfilectx
1288 return getfilectx
1281 getfilectx = lrugetfilectx()
1289 getfilectx = lrugetfilectx()
1282
1290
1283 ctx1 = repo[node1]
1291 ctx1 = repo[node1]
1284 ctx2 = repo[node2]
1292 ctx2 = repo[node2]
1285
1293
1286 if not changes:
1294 if not changes:
1287 changes = repo.status(ctx1, ctx2, match=match)
1295 changes = repo.status(ctx1, ctx2, match=match)
1288 modified, added, removed = changes[:3]
1296 modified, added, removed = changes[:3]
1289
1297
1290 if not modified and not added and not removed:
1298 if not modified and not added and not removed:
1291 return
1299 return []
1300
1301 revs = None
1302 if not repo.ui.quiet:
1303 hexfunc = repo.ui.debugflag and hex or short
1304 revs = [hexfunc(node) for node in [node1, node2] if node]
1305
1306 copy = {}
1307 if opts.git or opts.upgrade:
1308 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1309 copy = copy.copy()
1310 for k, v in copy.items():
1311 copy[v] = k
1312
1313 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1314 modified, added, removed, copy, getfilectx, opts, losedata)
1315 if opts.upgrade and not opts.git:
1316 try:
1317 def losedata(fn):
1318 if not losedatafn or not losedatafn(fn=fn):
1319 raise GitDiffRequired()
1320 # Buffer the whole output until we are sure it can be generated
1321 return list(difffn(opts.copy(git=False), losedata))
1322 except GitDiffRequired:
1323 return difffn(opts.copy(git=True), None)
1324 else:
1325 return difffn(opts, None)
1326
1327 def _addmodehdr(header, omode, nmode):
1328 if omode != nmode:
1329 header.append('old mode %s\n' % omode)
1330 header.append('new mode %s\n' % nmode)
1331
1332 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1333 copy, getfilectx, opts, losedatafn):
1292
1334
1293 date1 = util.datestr(ctx1.date())
1335 date1 = util.datestr(ctx1.date())
1294 man1 = ctx1.manifest()
1336 man1 = ctx1.manifest()
1295
1337
1296 revs = None
1338 gone = set()
1297 if not repo.ui.quiet and not opts.git:
1339 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1298 hexfunc = repo.ui.debugflag and hex or short
1299 revs = [hexfunc(node) for node in [node1, node2] if node]
1300
1340
1301 if opts.git:
1341 if opts.git:
1302 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
1342 revs = None
1303 copy = copy.copy()
1304 for k, v in copy.items():
1305 copy[v] = k
1306
1307 gone = set()
1308 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1309
1343
1310 for f in sorted(modified + added + removed):
1344 for f in sorted(modified + added + removed):
1311 to = None
1345 to = None
1312 tn = None
1346 tn = None
1313 dodiff = True
1347 dodiff = True
1314 header = []
1348 header = []
1315 if f in man1:
1349 if f in man1:
1316 to = getfilectx(f, ctx1).data()
1350 to = getfilectx(f, ctx1).data()
1317 if f not in removed:
1351 if f not in removed:
1318 tn = getfilectx(f, ctx2).data()
1352 tn = getfilectx(f, ctx2).data()
1319 a, b = f, f
1353 a, b = f, f
1320 if opts.git:
1354 if opts.git or losedatafn:
1321 if f in added:
1355 if f in added:
1322 mode = gitmode[ctx2.flags(f)]
1356 mode = gitmode[ctx2.flags(f)]
1323 if f in copy:
1357 if f in copy:
1324 a = copy[f]
1358 if opts.git:
1325 omode = gitmode[man1.flags(a)]
1359 a = copy[f]
1326 _addmodehdr(header, omode, mode)
1360 omode = gitmode[man1.flags(a)]
1327 if a in removed and a not in gone:
1361 _addmodehdr(header, omode, mode)
1328 op = 'rename'
1362 if a in removed and a not in gone:
1329 gone.add(a)
1363 op = 'rename'
1364 gone.add(a)
1365 else:
1366 op = 'copy'
1367 header.append('%s from %s\n' % (op, a))
1368 header.append('%s to %s\n' % (op, f))
1369 to = getfilectx(a, ctx1).data()
1330 else:
1370 else:
1331 op = 'copy'
1371 losedatafn(f)
1332 header.append('%s from %s\n' % (op, a))
1333 header.append('%s to %s\n' % (op, f))
1334 to = getfilectx(a, ctx1).data()
1335 else:
1372 else:
1336 header.append('new file mode %s\n' % mode)
1373 if opts.git:
1374 header.append('new file mode %s\n' % mode)
1375 elif ctx2.flags(f):
1376 losedatafn(f)
1337 if util.binary(tn):
1377 if util.binary(tn):
1338 dodiff = 'binary'
1378 if opts.git:
1379 dodiff = 'binary'
1380 else:
1381 losedatafn(f)
1382 if not opts.git and not tn:
1383 # regular diffs cannot represent new empty file
1384 losedatafn(f)
1339 elif f in removed:
1385 elif f in removed:
1340 # have we already reported a copy above?
1386 if opts.git:
1341 if f in copy and copy[f] in added and copy[copy[f]] == f:
1387 # have we already reported a copy above?
1342 dodiff = False
1388 if f in copy and copy[f] in added and copy[copy[f]] == f:
1343 else:
1389 dodiff = False
1344 header.append('deleted file mode %s\n' %
1390 else:
1345 gitmode[man1.flags(f)])
1391 header.append('deleted file mode %s\n' %
1392 gitmode[man1.flags(f)])
1393 elif not to:
1394 # regular diffs cannot represent empty file deletion
1395 losedatafn(f)
1346 else:
1396 else:
1347 omode = gitmode[man1.flags(f)]
1397 oflag = man1.flags(f)
1348 nmode = gitmode[ctx2.flags(f)]
1398 nflag = ctx2.flags(f)
1349 _addmodehdr(header, omode, nmode)
1399 binary = util.binary(to) or util.binary(tn)
1350 if util.binary(to) or util.binary(tn):
1400 if opts.git:
1351 dodiff = 'binary'
1401 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1352 header.insert(0, mdiff.diffline(revs, a, b, opts))
1402 if binary:
1403 dodiff = 'binary'
1404 elif binary or nflag != oflag:
1405 losedatafn(f)
1406 if opts.git:
1407 header.insert(0, mdiff.diffline(revs, a, b, opts))
1408
1353 if dodiff:
1409 if dodiff:
1354 if dodiff == 'binary':
1410 if dodiff == 'binary':
1355 text = b85diff(to, tn)
1411 text = b85diff(to, tn)
1356 else:
1412 else:
1357 text = mdiff.unidiff(to, date1,
1413 text = mdiff.unidiff(to, date1,
1358 # ctx2 date may be dynamic
1414 # ctx2 date may be dynamic
1359 tn, util.datestr(ctx2.date()),
1415 tn, util.datestr(ctx2.date()),
1360 a, b, revs, opts=opts)
1416 a, b, revs, opts=opts)
1361 if header and (text or len(header) > 1):
1417 if header and (text or len(header) > 1):
1362 yield ''.join(header)
1418 yield ''.join(header)
1363 if text:
1419 if text:
1364 yield text
1420 yield text
1365
1421
1366 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1422 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1367 opts=None):
1423 opts=None):
1368 '''export changesets as hg patches.'''
1424 '''export changesets as hg patches.'''
1369
1425
1370 total = len(revs)
1426 total = len(revs)
1371 revwidth = max([len(str(rev)) for rev in revs])
1427 revwidth = max([len(str(rev)) for rev in revs])
1372
1428
1373 def single(rev, seqno, fp):
1429 def single(rev, seqno, fp):
1374 ctx = repo[rev]
1430 ctx = repo[rev]
1375 node = ctx.node()
1431 node = ctx.node()
1376 parents = [p.node() for p in ctx.parents() if p]
1432 parents = [p.node() for p in ctx.parents() if p]
1377 branch = ctx.branch()
1433 branch = ctx.branch()
1378 if switch_parent:
1434 if switch_parent:
1379 parents.reverse()
1435 parents.reverse()
1380 prev = (parents and parents[0]) or nullid
1436 prev = (parents and parents[0]) or nullid
1381
1437
1382 if not fp:
1438 if not fp:
1383 fp = cmdutil.make_file(repo, template, node, total=total,
1439 fp = cmdutil.make_file(repo, template, node, total=total,
1384 seqno=seqno, revwidth=revwidth,
1440 seqno=seqno, revwidth=revwidth,
1385 mode='ab')
1441 mode='ab')
1386 if fp != sys.stdout and hasattr(fp, 'name'):
1442 if fp != sys.stdout and hasattr(fp, 'name'):
1387 repo.ui.note("%s\n" % fp.name)
1443 repo.ui.note("%s\n" % fp.name)
1388
1444
1389 fp.write("# HG changeset patch\n")
1445 fp.write("# HG changeset patch\n")
1390 fp.write("# User %s\n" % ctx.user())
1446 fp.write("# User %s\n" % ctx.user())
1391 fp.write("# Date %d %d\n" % ctx.date())
1447 fp.write("# Date %d %d\n" % ctx.date())
1392 if branch and (branch != 'default'):
1448 if branch and (branch != 'default'):
1393 fp.write("# Branch %s\n" % branch)
1449 fp.write("# Branch %s\n" % branch)
1394 fp.write("# Node ID %s\n" % hex(node))
1450 fp.write("# Node ID %s\n" % hex(node))
1395 fp.write("# Parent %s\n" % hex(prev))
1451 fp.write("# Parent %s\n" % hex(prev))
1396 if len(parents) > 1:
1452 if len(parents) > 1:
1397 fp.write("# Parent %s\n" % hex(parents[1]))
1453 fp.write("# Parent %s\n" % hex(parents[1]))
1398 fp.write(ctx.description().rstrip())
1454 fp.write(ctx.description().rstrip())
1399 fp.write("\n\n")
1455 fp.write("\n\n")
1400
1456
1401 for chunk in diff(repo, prev, node, opts=opts):
1457 for chunk in diff(repo, prev, node, opts=opts):
1402 fp.write(chunk)
1458 fp.write(chunk)
1403
1459
1404 for seqno, rev in enumerate(revs):
1460 for seqno, rev in enumerate(revs):
1405 single(rev, seqno+1, fp)
1461 single(rev, seqno+1, fp)
1406
1462
1407 def diffstatdata(lines):
1463 def diffstatdata(lines):
1408 filename, adds, removes = None, 0, 0
1464 filename, adds, removes = None, 0, 0
1409 for line in lines:
1465 for line in lines:
1410 if line.startswith('diff'):
1466 if line.startswith('diff'):
1411 if filename:
1467 if filename:
1412 isbinary = adds == 0 and removes == 0
1468 isbinary = adds == 0 and removes == 0
1413 yield (filename, adds, removes, isbinary)
1469 yield (filename, adds, removes, isbinary)
1414 # set numbers to 0 anyway when starting new file
1470 # set numbers to 0 anyway when starting new file
1415 adds, removes = 0, 0
1471 adds, removes = 0, 0
1416 if line.startswith('diff --git'):
1472 if line.startswith('diff --git'):
1417 filename = gitre.search(line).group(1)
1473 filename = gitre.search(line).group(1)
1418 else:
1474 else:
1419 # format: "diff -r ... -r ... filename"
1475 # format: "diff -r ... -r ... filename"
1420 filename = line.split(None, 5)[-1]
1476 filename = line.split(None, 5)[-1]
1421 elif line.startswith('+') and not line.startswith('+++'):
1477 elif line.startswith('+') and not line.startswith('+++'):
1422 adds += 1
1478 adds += 1
1423 elif line.startswith('-') and not line.startswith('---'):
1479 elif line.startswith('-') and not line.startswith('---'):
1424 removes += 1
1480 removes += 1
1425 if filename:
1481 if filename:
1426 isbinary = adds == 0 and removes == 0
1482 isbinary = adds == 0 and removes == 0
1427 yield (filename, adds, removes, isbinary)
1483 yield (filename, adds, removes, isbinary)
1428
1484
1429 def diffstat(lines, width=80, git=False):
1485 def diffstat(lines, width=80, git=False):
1430 output = []
1486 output = []
1431 stats = list(diffstatdata(lines))
1487 stats = list(diffstatdata(lines))
1432
1488
1433 maxtotal, maxname = 0, 0
1489 maxtotal, maxname = 0, 0
1434 totaladds, totalremoves = 0, 0
1490 totaladds, totalremoves = 0, 0
1435 hasbinary = False
1491 hasbinary = False
1436 for filename, adds, removes, isbinary in stats:
1492 for filename, adds, removes, isbinary in stats:
1437 totaladds += adds
1493 totaladds += adds
1438 totalremoves += removes
1494 totalremoves += removes
1439 maxname = max(maxname, len(filename))
1495 maxname = max(maxname, len(filename))
1440 maxtotal = max(maxtotal, adds+removes)
1496 maxtotal = max(maxtotal, adds+removes)
1441 if isbinary:
1497 if isbinary:
1442 hasbinary = True
1498 hasbinary = True
1443
1499
1444 countwidth = len(str(maxtotal))
1500 countwidth = len(str(maxtotal))
1445 if hasbinary and countwidth < 3:
1501 if hasbinary and countwidth < 3:
1446 countwidth = 3
1502 countwidth = 3
1447 graphwidth = width - countwidth - maxname - 6
1503 graphwidth = width - countwidth - maxname - 6
1448 if graphwidth < 10:
1504 if graphwidth < 10:
1449 graphwidth = 10
1505 graphwidth = 10
1450
1506
1451 def scale(i):
1507 def scale(i):
1452 if maxtotal <= graphwidth:
1508 if maxtotal <= graphwidth:
1453 return i
1509 return i
1454 # If diffstat runs out of room it doesn't print anything,
1510 # If diffstat runs out of room it doesn't print anything,
1455 # which isn't very useful, so always print at least one + or -
1511 # which isn't very useful, so always print at least one + or -
1456 # if there were at least some changes.
1512 # if there were at least some changes.
1457 return max(i * graphwidth // maxtotal, int(bool(i)))
1513 return max(i * graphwidth // maxtotal, int(bool(i)))
1458
1514
1459 for filename, adds, removes, isbinary in stats:
1515 for filename, adds, removes, isbinary in stats:
1460 if git and isbinary:
1516 if git and isbinary:
1461 count = 'Bin'
1517 count = 'Bin'
1462 else:
1518 else:
1463 count = adds + removes
1519 count = adds + removes
1464 pluses = '+' * scale(adds)
1520 pluses = '+' * scale(adds)
1465 minuses = '-' * scale(removes)
1521 minuses = '-' * scale(removes)
1466 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1522 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1467 count, pluses, minuses))
1523 count, pluses, minuses))
1468
1524
1469 if stats:
1525 if stats:
1470 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1526 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1471 % (len(stats), totaladds, totalremoves))
1527 % (len(stats), totaladds, totalremoves))
1472
1528
1473 return ''.join(output)
1529 return ''.join(output)
General Comments 0
You need to be logged in to leave comments. Login now