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