##// END OF EJS Templates
patch: better handling of sequence of offset patch hunks (issue1941)...
Greg Onufer -
r10135:9a4034b6 stable
parent child Browse files
Show More
@@ -0,0 +1,67 b''
1 #!/bin/sh
2
3 cat > writepatterns.py <<EOF
4 import sys
5
6 path = sys.argv[1]
7 patterns = sys.argv[2:]
8
9 fp = file(path, 'wb')
10 for pattern in patterns:
11 count = int(pattern[0:-1])
12 char = pattern[-1] + '\n'
13 fp.write(char*count)
14 fp.close()
15 EOF
16
17 echo % prepare repo
18 hg init a
19 cd a
20
21 # These initial lines of Xs were not in the original file used to generate
22 # the patch. So all the patch hunks need to be applied to a constant offset
23 # within this file. If the offset isn't tracked then the hunks can be
24 # applied to the wrong lines of this file.
25 python ../writepatterns.py a 34X 10A 1B 10A 1C 10A 1B 10A 1D 10A 1B 10A 1E 10A 1B 10A
26 hg commit -Am adda
27
28 # This is a cleaner patch generated via diff
29 # In this case it reproduces the problem when
30 # the output of hg export does not
31 echo % import patch
32 hg import -v -m 'b' -d '2 0' - <<EOF
33 --- a/a 2009-12-08 19:26:17.000000000 -0800
34 +++ b/a 2009-12-08 19:26:17.000000000 -0800
35 @@ -9,7 +9,7 @@
36 A
37 A
38 B
39 -A
40 +a
41 A
42 A
43 A
44 @@ -53,7 +53,7 @@
45 A
46 A
47 B
48 -A
49 +a
50 A
51 A
52 A
53 @@ -75,7 +75,7 @@
54 A
55 A
56 B
57 -A
58 +a
59 A
60 A
61 A
62 EOF
63
64 echo % compare imported changes against reference file
65 python ../writepatterns.py aref 34X 10A 1B 1a 9A 1C 10A 1B 10A 1D 10A 1B 1a 9A 1E 10A 1B 1a 9A
66 diff -u aref a
67
@@ -0,0 +1,10 b''
1 % prepare repo
2 adding a
3 % import patch
4 applying patch from stdin
5 patching file a
6 Hunk #1 succeeded at 43 (offset 34 lines).
7 Hunk #2 succeeded at 87 (offset 34 lines).
8 Hunk #3 succeeded at 109 (offset 34 lines).
9 a
10 % compare imported changes against reference file
@@ -1,1430 +1,1435 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
243 243 def push(self, line):
244 244 if line is not None:
245 245 self.buf.append(line)
246 246
247 247 def readline(self):
248 248 if self.buf:
249 249 l = self.buf[0]
250 250 del self.buf[0]
251 251 return l
252 252 l = self.fp.readline()
253 253 if self.textmode and l.endswith('\r\n'):
254 254 l = l[:-2] + '\n'
255 255 return l
256 256
257 257 def __iter__(self):
258 258 while 1:
259 259 l = self.readline()
260 260 if not l:
261 261 break
262 262 yield l
263 263
264 264 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
265 265 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
266 266 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
267 267
268 268 class patchfile(object):
269 269 def __init__(self, ui, fname, opener, missing=False, eol=None):
270 270 self.fname = fname
271 271 self.eol = eol
272 272 self.opener = opener
273 273 self.ui = ui
274 274 self.lines = []
275 275 self.exists = False
276 276 self.missing = missing
277 277 if not missing:
278 278 try:
279 279 self.lines = self.readlines(fname)
280 280 self.exists = True
281 281 except IOError:
282 282 pass
283 283 else:
284 284 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
285 285
286 286 self.hash = {}
287 287 self.dirty = 0
288 288 self.offset = 0
289 self.skew = 0
289 290 self.rej = []
290 291 self.fileprinted = False
291 292 self.printfile(False)
292 293 self.hunks = 0
293 294
294 295 def readlines(self, fname):
295 296 if os.path.islink(fname):
296 297 return [os.readlink(fname)]
297 298 fp = self.opener(fname, 'r')
298 299 try:
299 300 return list(linereader(fp, self.eol is not None))
300 301 finally:
301 302 fp.close()
302 303
303 304 def writelines(self, fname, lines):
304 305 # Ensure supplied data ends in fname, being a regular file or
305 306 # a symlink. updatedir() will -too magically- take care of
306 307 # setting it to the proper type afterwards.
307 308 islink = os.path.islink(fname)
308 309 if islink:
309 310 fp = cStringIO.StringIO()
310 311 else:
311 312 fp = self.opener(fname, 'w')
312 313 try:
313 314 if self.eol and self.eol != '\n':
314 315 for l in lines:
315 316 if l and l[-1] == '\n':
316 317 l = l[:-1] + self.eol
317 318 fp.write(l)
318 319 else:
319 320 fp.writelines(lines)
320 321 if islink:
321 322 self.opener.symlink(fp.getvalue(), fname)
322 323 finally:
323 324 fp.close()
324 325
325 326 def unlink(self, fname):
326 327 os.unlink(fname)
327 328
328 329 def printfile(self, warn):
329 330 if self.fileprinted:
330 331 return
331 332 if warn or self.ui.verbose:
332 333 self.fileprinted = True
333 334 s = _("patching file %s\n") % self.fname
334 335 if warn:
335 336 self.ui.warn(s)
336 337 else:
337 338 self.ui.note(s)
338 339
339 340
340 341 def findlines(self, l, linenum):
341 342 # looks through the hash and finds candidate lines. The
342 343 # result is a list of line numbers sorted based on distance
343 344 # from linenum
344 345
345 346 cand = self.hash.get(l, [])
346 347 if len(cand) > 1:
347 348 # resort our list of potentials forward then back.
348 349 cand.sort(key=lambda x: abs(x - linenum))
349 350 return cand
350 351
351 352 def hashlines(self):
352 353 self.hash = {}
353 354 for x, s in enumerate(self.lines):
354 355 self.hash.setdefault(s, []).append(x)
355 356
356 357 def write_rej(self):
357 358 # our rejects are a little different from patch(1). This always
358 359 # creates rejects in the same form as the original patch. A file
359 360 # header is inserted so that you can run the reject through patch again
360 361 # without having to type the filename.
361 362
362 363 if not self.rej:
363 364 return
364 365
365 366 fname = self.fname + ".rej"
366 367 self.ui.warn(
367 368 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
368 369 (len(self.rej), self.hunks, fname))
369 370
370 371 def rejlines():
371 372 base = os.path.basename(self.fname)
372 373 yield "--- %s\n+++ %s\n" % (base, base)
373 374 for x in self.rej:
374 375 for l in x.hunk:
375 376 yield l
376 377 if l[-1] != '\n':
377 378 yield "\n\ No newline at end of file\n"
378 379
379 380 self.writelines(fname, rejlines())
380 381
381 382 def write(self, dest=None):
382 383 if not self.dirty:
383 384 return
384 385 if not dest:
385 386 dest = self.fname
386 387 self.writelines(dest, self.lines)
387 388
388 389 def close(self):
389 390 self.write()
390 391 self.write_rej()
391 392
392 393 def apply(self, h):
393 394 if not h.complete():
394 395 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
395 396 (h.number, h.desc, len(h.a), h.lena, len(h.b),
396 397 h.lenb))
397 398
398 399 self.hunks += 1
399 400
400 401 if self.missing:
401 402 self.rej.append(h)
402 403 return -1
403 404
404 405 if self.exists and h.createfile():
405 406 self.ui.warn(_("file %s already exists\n") % self.fname)
406 407 self.rej.append(h)
407 408 return -1
408 409
409 410 if isinstance(h, binhunk):
410 411 if h.rmfile():
411 412 self.unlink(self.fname)
412 413 else:
413 414 self.lines[:] = h.new()
414 415 self.offset += len(h.new())
415 416 self.dirty = 1
416 417 return 0
417 418
418 419 # fast case first, no offsets, no fuzz
419 420 old = h.old()
420 421 # patch starts counting at 1 unless we are adding the file
421 422 if h.starta == 0:
422 423 start = 0
423 424 else:
424 425 start = h.starta + self.offset - 1
425 426 orig_start = start
426 if diffhelpers.testhunk(old, self.lines, start) == 0:
427 # if there's skew we want to emit the "(offset %d lines)" even
428 # when the hunk cleanly applies at start + skew, so skip the
429 # fast case code
430 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
427 431 if h.rmfile():
428 432 self.unlink(self.fname)
429 433 else:
430 434 self.lines[start : start + h.lena] = h.new()
431 435 self.offset += h.lenb - h.lena
432 436 self.dirty = 1
433 437 return 0
434 438
435 439 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
436 440 self.hashlines()
437 441 if h.hunk[-1][0] != ' ':
438 442 # if the hunk tried to put something at the bottom of the file
439 443 # override the start line and use eof here
440 444 search_start = len(self.lines)
441 445 else:
442 search_start = orig_start
446 search_start = orig_start + self.skew
443 447
444 448 for fuzzlen in xrange(3):
445 449 for toponly in [ True, False ]:
446 450 old = h.old(fuzzlen, toponly)
447 451
448 452 cand = self.findlines(old[0][1:], search_start)
449 453 for l in cand:
450 454 if diffhelpers.testhunk(old, self.lines, l) == 0:
451 455 newlines = h.new(fuzzlen, toponly)
452 456 self.lines[l : l + len(old)] = newlines
453 457 self.offset += len(newlines) - len(old)
458 self.skew = l - orig_start
454 459 self.dirty = 1
455 460 if fuzzlen:
456 461 fuzzstr = "with fuzz %d " % fuzzlen
457 462 f = self.ui.warn
458 463 self.printfile(True)
459 464 else:
460 465 fuzzstr = ""
461 466 f = self.ui.note
462 467 offset = l - orig_start - fuzzlen
463 468 if offset == 1:
464 469 msg = _("Hunk #%d succeeded at %d %s"
465 470 "(offset %d line).\n")
466 471 else:
467 472 msg = _("Hunk #%d succeeded at %d %s"
468 473 "(offset %d lines).\n")
469 474 f(msg % (h.number, l+1, fuzzstr, offset))
470 475 return fuzzlen
471 476 self.printfile(True)
472 477 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
473 478 self.rej.append(h)
474 479 return -1
475 480
476 481 class hunk(object):
477 482 def __init__(self, desc, num, lr, context, create=False, remove=False):
478 483 self.number = num
479 484 self.desc = desc
480 485 self.hunk = [ desc ]
481 486 self.a = []
482 487 self.b = []
483 488 self.starta = self.lena = None
484 489 self.startb = self.lenb = None
485 490 if context:
486 491 self.read_context_hunk(lr)
487 492 else:
488 493 self.read_unified_hunk(lr)
489 494 self.create = create
490 495 self.remove = remove and not create
491 496
492 497 def read_unified_hunk(self, lr):
493 498 m = unidesc.match(self.desc)
494 499 if not m:
495 500 raise PatchError(_("bad hunk #%d") % self.number)
496 501 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
497 502 if self.lena is None:
498 503 self.lena = 1
499 504 else:
500 505 self.lena = int(self.lena)
501 506 if self.lenb is None:
502 507 self.lenb = 1
503 508 else:
504 509 self.lenb = int(self.lenb)
505 510 self.starta = int(self.starta)
506 511 self.startb = int(self.startb)
507 512 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
508 513 # if we hit eof before finishing out the hunk, the last line will
509 514 # be zero length. Lets try to fix it up.
510 515 while len(self.hunk[-1]) == 0:
511 516 del self.hunk[-1]
512 517 del self.a[-1]
513 518 del self.b[-1]
514 519 self.lena -= 1
515 520 self.lenb -= 1
516 521
517 522 def read_context_hunk(self, lr):
518 523 self.desc = lr.readline()
519 524 m = contextdesc.match(self.desc)
520 525 if not m:
521 526 raise PatchError(_("bad hunk #%d") % self.number)
522 527 foo, self.starta, foo2, aend, foo3 = m.groups()
523 528 self.starta = int(self.starta)
524 529 if aend is None:
525 530 aend = self.starta
526 531 self.lena = int(aend) - self.starta
527 532 if self.starta:
528 533 self.lena += 1
529 534 for x in xrange(self.lena):
530 535 l = lr.readline()
531 536 if l.startswith('---'):
532 537 lr.push(l)
533 538 break
534 539 s = l[2:]
535 540 if l.startswith('- ') or l.startswith('! '):
536 541 u = '-' + s
537 542 elif l.startswith(' '):
538 543 u = ' ' + s
539 544 else:
540 545 raise PatchError(_("bad hunk #%d old text line %d") %
541 546 (self.number, x))
542 547 self.a.append(u)
543 548 self.hunk.append(u)
544 549
545 550 l = lr.readline()
546 551 if l.startswith('\ '):
547 552 s = self.a[-1][:-1]
548 553 self.a[-1] = s
549 554 self.hunk[-1] = s
550 555 l = lr.readline()
551 556 m = contextdesc.match(l)
552 557 if not m:
553 558 raise PatchError(_("bad hunk #%d") % self.number)
554 559 foo, self.startb, foo2, bend, foo3 = m.groups()
555 560 self.startb = int(self.startb)
556 561 if bend is None:
557 562 bend = self.startb
558 563 self.lenb = int(bend) - self.startb
559 564 if self.startb:
560 565 self.lenb += 1
561 566 hunki = 1
562 567 for x in xrange(self.lenb):
563 568 l = lr.readline()
564 569 if l.startswith('\ '):
565 570 s = self.b[-1][:-1]
566 571 self.b[-1] = s
567 572 self.hunk[hunki-1] = s
568 573 continue
569 574 if not l:
570 575 lr.push(l)
571 576 break
572 577 s = l[2:]
573 578 if l.startswith('+ ') or l.startswith('! '):
574 579 u = '+' + s
575 580 elif l.startswith(' '):
576 581 u = ' ' + s
577 582 elif len(self.b) == 0:
578 583 # this can happen when the hunk does not add any lines
579 584 lr.push(l)
580 585 break
581 586 else:
582 587 raise PatchError(_("bad hunk #%d old text line %d") %
583 588 (self.number, x))
584 589 self.b.append(s)
585 590 while True:
586 591 if hunki >= len(self.hunk):
587 592 h = ""
588 593 else:
589 594 h = self.hunk[hunki]
590 595 hunki += 1
591 596 if h == u:
592 597 break
593 598 elif h.startswith('-'):
594 599 continue
595 600 else:
596 601 self.hunk.insert(hunki-1, u)
597 602 break
598 603
599 604 if not self.a:
600 605 # this happens when lines were only added to the hunk
601 606 for x in self.hunk:
602 607 if x.startswith('-') or x.startswith(' '):
603 608 self.a.append(x)
604 609 if not self.b:
605 610 # this happens when lines were only deleted from the hunk
606 611 for x in self.hunk:
607 612 if x.startswith('+') or x.startswith(' '):
608 613 self.b.append(x[1:])
609 614 # @@ -start,len +start,len @@
610 615 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
611 616 self.startb, self.lenb)
612 617 self.hunk[0] = self.desc
613 618
614 619 def fix_newline(self):
615 620 diffhelpers.fix_newline(self.hunk, self.a, self.b)
616 621
617 622 def complete(self):
618 623 return len(self.a) == self.lena and len(self.b) == self.lenb
619 624
620 625 def createfile(self):
621 626 return self.starta == 0 and self.lena == 0 and self.create
622 627
623 628 def rmfile(self):
624 629 return self.startb == 0 and self.lenb == 0 and self.remove
625 630
626 631 def fuzzit(self, l, fuzz, toponly):
627 632 # this removes context lines from the top and bottom of list 'l'. It
628 633 # checks the hunk to make sure only context lines are removed, and then
629 634 # returns a new shortened list of lines.
630 635 fuzz = min(fuzz, len(l)-1)
631 636 if fuzz:
632 637 top = 0
633 638 bot = 0
634 639 hlen = len(self.hunk)
635 640 for x in xrange(hlen-1):
636 641 # the hunk starts with the @@ line, so use x+1
637 642 if self.hunk[x+1][0] == ' ':
638 643 top += 1
639 644 else:
640 645 break
641 646 if not toponly:
642 647 for x in xrange(hlen-1):
643 648 if self.hunk[hlen-bot-1][0] == ' ':
644 649 bot += 1
645 650 else:
646 651 break
647 652
648 653 # top and bot now count context in the hunk
649 654 # adjust them if either one is short
650 655 context = max(top, bot, 3)
651 656 if bot < context:
652 657 bot = max(0, fuzz - (context - bot))
653 658 else:
654 659 bot = min(fuzz, bot)
655 660 if top < context:
656 661 top = max(0, fuzz - (context - top))
657 662 else:
658 663 top = min(fuzz, top)
659 664
660 665 return l[top:len(l)-bot]
661 666 return l
662 667
663 668 def old(self, fuzz=0, toponly=False):
664 669 return self.fuzzit(self.a, fuzz, toponly)
665 670
666 671 def newctrl(self):
667 672 res = []
668 673 for x in self.hunk:
669 674 c = x[0]
670 675 if c == ' ' or c == '+':
671 676 res.append(x)
672 677 return res
673 678
674 679 def new(self, fuzz=0, toponly=False):
675 680 return self.fuzzit(self.b, fuzz, toponly)
676 681
677 682 class binhunk:
678 683 'A binary patch file. Only understands literals so far.'
679 684 def __init__(self, gitpatch):
680 685 self.gitpatch = gitpatch
681 686 self.text = None
682 687 self.hunk = ['GIT binary patch\n']
683 688
684 689 def createfile(self):
685 690 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
686 691
687 692 def rmfile(self):
688 693 return self.gitpatch.op == 'DELETE'
689 694
690 695 def complete(self):
691 696 return self.text is not None
692 697
693 698 def new(self):
694 699 return [self.text]
695 700
696 701 def extract(self, lr):
697 702 line = lr.readline()
698 703 self.hunk.append(line)
699 704 while line and not line.startswith('literal '):
700 705 line = lr.readline()
701 706 self.hunk.append(line)
702 707 if not line:
703 708 raise PatchError(_('could not extract binary patch'))
704 709 size = int(line[8:].rstrip())
705 710 dec = []
706 711 line = lr.readline()
707 712 self.hunk.append(line)
708 713 while len(line) > 1:
709 714 l = line[0]
710 715 if l <= 'Z' and l >= 'A':
711 716 l = ord(l) - ord('A') + 1
712 717 else:
713 718 l = ord(l) - ord('a') + 27
714 719 dec.append(base85.b85decode(line[1:-1])[:l])
715 720 line = lr.readline()
716 721 self.hunk.append(line)
717 722 text = zlib.decompress(''.join(dec))
718 723 if len(text) != size:
719 724 raise PatchError(_('binary patch is %d bytes, not %d') %
720 725 len(text), size)
721 726 self.text = text
722 727
723 728 def parsefilename(str):
724 729 # --- filename \t|space stuff
725 730 s = str[4:].rstrip('\r\n')
726 731 i = s.find('\t')
727 732 if i < 0:
728 733 i = s.find(' ')
729 734 if i < 0:
730 735 return s
731 736 return s[:i]
732 737
733 738 def selectfile(afile_orig, bfile_orig, hunk, strip):
734 739 def pathstrip(path, count=1):
735 740 pathlen = len(path)
736 741 i = 0
737 742 if count == 0:
738 743 return '', path.rstrip()
739 744 while count > 0:
740 745 i = path.find('/', i)
741 746 if i == -1:
742 747 raise PatchError(_("unable to strip away %d dirs from %s") %
743 748 (count, path))
744 749 i += 1
745 750 # consume '//' in the path
746 751 while i < pathlen - 1 and path[i] == '/':
747 752 i += 1
748 753 count -= 1
749 754 return path[:i].lstrip(), path[i:].rstrip()
750 755
751 756 nulla = afile_orig == "/dev/null"
752 757 nullb = bfile_orig == "/dev/null"
753 758 abase, afile = pathstrip(afile_orig, strip)
754 759 gooda = not nulla and util.lexists(afile)
755 760 bbase, bfile = pathstrip(bfile_orig, strip)
756 761 if afile == bfile:
757 762 goodb = gooda
758 763 else:
759 764 goodb = not nullb and os.path.exists(bfile)
760 765 createfunc = hunk.createfile
761 766 missing = not goodb and not gooda and not createfunc()
762 767
763 768 # some diff programs apparently produce create patches where the
764 769 # afile is not /dev/null, but rather the same name as the bfile
765 770 if missing and afile == bfile:
766 771 # this isn't very pretty
767 772 hunk.create = True
768 773 if createfunc():
769 774 missing = False
770 775 else:
771 776 hunk.create = False
772 777
773 778 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
774 779 # diff is between a file and its backup. In this case, the original
775 780 # file should be patched (see original mpatch code).
776 781 isbackup = (abase == bbase and bfile.startswith(afile))
777 782 fname = None
778 783 if not missing:
779 784 if gooda and goodb:
780 785 fname = isbackup and afile or bfile
781 786 elif gooda:
782 787 fname = afile
783 788
784 789 if not fname:
785 790 if not nullb:
786 791 fname = isbackup and afile or bfile
787 792 elif not nulla:
788 793 fname = afile
789 794 else:
790 795 raise PatchError(_("undefined source and destination files"))
791 796
792 797 return fname, missing
793 798
794 799 def scangitpatch(lr, firstline):
795 800 """
796 801 Git patches can emit:
797 802 - rename a to b
798 803 - change b
799 804 - copy a to c
800 805 - change c
801 806
802 807 We cannot apply this sequence as-is, the renamed 'a' could not be
803 808 found for it would have been renamed already. And we cannot copy
804 809 from 'b' instead because 'b' would have been changed already. So
805 810 we scan the git patch for copy and rename commands so we can
806 811 perform the copies ahead of time.
807 812 """
808 813 pos = 0
809 814 try:
810 815 pos = lr.fp.tell()
811 816 fp = lr.fp
812 817 except IOError:
813 818 fp = cStringIO.StringIO(lr.fp.read())
814 819 gitlr = linereader(fp, lr.textmode)
815 820 gitlr.push(firstline)
816 821 (dopatch, gitpatches) = readgitpatch(gitlr)
817 822 fp.seek(pos)
818 823 return dopatch, gitpatches
819 824
820 825 def iterhunks(ui, fp, sourcefile=None, textmode=False):
821 826 """Read a patch and yield the following events:
822 827 - ("file", afile, bfile, firsthunk): select a new target file.
823 828 - ("hunk", hunk): a new hunk is ready to be applied, follows a
824 829 "file" event.
825 830 - ("git", gitchanges): current diff is in git format, gitchanges
826 831 maps filenames to gitpatch records. Unique event.
827 832
828 833 If textmode is True, input line-endings are normalized to LF.
829 834 """
830 835 changed = {}
831 836 current_hunk = None
832 837 afile = ""
833 838 bfile = ""
834 839 state = None
835 840 hunknum = 0
836 841 emitfile = False
837 842 git = False
838 843
839 844 # our states
840 845 BFILE = 1
841 846 context = None
842 847 lr = linereader(fp, textmode)
843 848 dopatch = True
844 849 # gitworkdone is True if a git operation (copy, rename, ...) was
845 850 # performed already for the current file. Useful when the file
846 851 # section may have no hunk.
847 852 gitworkdone = False
848 853
849 854 while True:
850 855 newfile = False
851 856 x = lr.readline()
852 857 if not x:
853 858 break
854 859 if current_hunk:
855 860 if x.startswith('\ '):
856 861 current_hunk.fix_newline()
857 862 yield 'hunk', current_hunk
858 863 current_hunk = None
859 864 gitworkdone = False
860 865 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
861 866 ((context is not False) and x.startswith('***************')))):
862 867 try:
863 868 if context is None and x.startswith('***************'):
864 869 context = True
865 870 gpatch = changed.get(bfile)
866 871 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
867 872 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
868 873 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
869 874 except PatchError, err:
870 875 ui.debug(err)
871 876 current_hunk = None
872 877 continue
873 878 hunknum += 1
874 879 if emitfile:
875 880 emitfile = False
876 881 yield 'file', (afile, bfile, current_hunk)
877 882 elif state == BFILE and x.startswith('GIT binary patch'):
878 883 current_hunk = binhunk(changed[bfile])
879 884 hunknum += 1
880 885 if emitfile:
881 886 emitfile = False
882 887 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
883 888 current_hunk.extract(lr)
884 889 elif x.startswith('diff --git'):
885 890 # check for git diff, scanning the whole patch file if needed
886 891 m = gitre.match(x)
887 892 if m:
888 893 afile, bfile = m.group(1, 2)
889 894 if not git:
890 895 git = True
891 896 dopatch, gitpatches = scangitpatch(lr, x)
892 897 yield 'git', gitpatches
893 898 for gp in gitpatches:
894 899 changed[gp.path] = gp
895 900 # else error?
896 901 # copy/rename + modify should modify target, not source
897 902 gp = changed.get(bfile)
898 903 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
899 904 afile = bfile
900 905 gitworkdone = True
901 906 newfile = True
902 907 elif x.startswith('---'):
903 908 # check for a unified diff
904 909 l2 = lr.readline()
905 910 if not l2.startswith('+++'):
906 911 lr.push(l2)
907 912 continue
908 913 newfile = True
909 914 context = False
910 915 afile = parsefilename(x)
911 916 bfile = parsefilename(l2)
912 917 elif x.startswith('***'):
913 918 # check for a context diff
914 919 l2 = lr.readline()
915 920 if not l2.startswith('---'):
916 921 lr.push(l2)
917 922 continue
918 923 l3 = lr.readline()
919 924 lr.push(l3)
920 925 if not l3.startswith("***************"):
921 926 lr.push(l2)
922 927 continue
923 928 newfile = True
924 929 context = True
925 930 afile = parsefilename(x)
926 931 bfile = parsefilename(l2)
927 932
928 933 if newfile:
929 934 emitfile = True
930 935 state = BFILE
931 936 hunknum = 0
932 937 if current_hunk:
933 938 if current_hunk.complete():
934 939 yield 'hunk', current_hunk
935 940 else:
936 941 raise PatchError(_("malformed patch %s %s") % (afile,
937 942 current_hunk.desc))
938 943
939 944 if hunknum == 0 and dopatch and not gitworkdone:
940 945 raise NoHunks
941 946
942 947 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eol=None):
943 948 """
944 949 Reads a patch from fp and tries to apply it.
945 950
946 951 The dict 'changed' is filled in with all of the filenames changed
947 952 by the patch. Returns 0 for a clean patch, -1 if any rejects were
948 953 found and 1 if there was any fuzz.
949 954
950 955 If 'eol' is None, the patch content and patched file are read in
951 956 binary mode. Otherwise, line endings are ignored when patching then
952 957 normalized to 'eol' (usually '\n' or \r\n').
953 958 """
954 959 rejects = 0
955 960 err = 0
956 961 current_file = None
957 962 gitpatches = None
958 963 opener = util.opener(os.getcwd())
959 964 textmode = eol is not None
960 965
961 966 def closefile():
962 967 if not current_file:
963 968 return 0
964 969 current_file.close()
965 970 return len(current_file.rej)
966 971
967 972 for state, values in iterhunks(ui, fp, sourcefile, textmode):
968 973 if state == 'hunk':
969 974 if not current_file:
970 975 continue
971 976 current_hunk = values
972 977 ret = current_file.apply(current_hunk)
973 978 if ret >= 0:
974 979 changed.setdefault(current_file.fname, None)
975 980 if ret > 0:
976 981 err = 1
977 982 elif state == 'file':
978 983 rejects += closefile()
979 984 afile, bfile, first_hunk = values
980 985 try:
981 986 if sourcefile:
982 987 current_file = patchfile(ui, sourcefile, opener, eol=eol)
983 988 else:
984 989 current_file, missing = selectfile(afile, bfile, first_hunk,
985 990 strip)
986 991 current_file = patchfile(ui, current_file, opener, missing, eol)
987 992 except PatchError, err:
988 993 ui.warn(str(err) + '\n')
989 994 current_file, current_hunk = None, None
990 995 rejects += 1
991 996 continue
992 997 elif state == 'git':
993 998 gitpatches = values
994 999 cwd = os.getcwd()
995 1000 for gp in gitpatches:
996 1001 if gp.op in ('COPY', 'RENAME'):
997 1002 copyfile(gp.oldpath, gp.path, cwd)
998 1003 changed[gp.path] = gp
999 1004 else:
1000 1005 raise util.Abort(_('unsupported parser state: %s') % state)
1001 1006
1002 1007 rejects += closefile()
1003 1008
1004 1009 if rejects:
1005 1010 return -1
1006 1011 return err
1007 1012
1008 1013 def diffopts(ui, opts=None, untrusted=False):
1009 1014 def get(key, name=None, getter=ui.configbool):
1010 1015 return ((opts and opts.get(key)) or
1011 1016 getter('diff', name or key, None, untrusted=untrusted))
1012 1017 return mdiff.diffopts(
1013 1018 text=opts and opts.get('text'),
1014 1019 git=get('git'),
1015 1020 nodates=get('nodates'),
1016 1021 showfunc=get('show_function', 'showfunc'),
1017 1022 ignorews=get('ignore_all_space', 'ignorews'),
1018 1023 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1019 1024 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1020 1025 context=get('unified', getter=ui.config))
1021 1026
1022 1027 def updatedir(ui, repo, patches, similarity=0):
1023 1028 '''Update dirstate after patch application according to metadata'''
1024 1029 if not patches:
1025 1030 return
1026 1031 copies = []
1027 1032 removes = set()
1028 1033 cfiles = patches.keys()
1029 1034 cwd = repo.getcwd()
1030 1035 if cwd:
1031 1036 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1032 1037 for f in patches:
1033 1038 gp = patches[f]
1034 1039 if not gp:
1035 1040 continue
1036 1041 if gp.op == 'RENAME':
1037 1042 copies.append((gp.oldpath, gp.path))
1038 1043 removes.add(gp.oldpath)
1039 1044 elif gp.op == 'COPY':
1040 1045 copies.append((gp.oldpath, gp.path))
1041 1046 elif gp.op == 'DELETE':
1042 1047 removes.add(gp.path)
1043 1048 for src, dst in copies:
1044 1049 repo.copy(src, dst)
1045 1050 if (not similarity) and removes:
1046 1051 repo.remove(sorted(removes), True)
1047 1052 for f in patches:
1048 1053 gp = patches[f]
1049 1054 if gp and gp.mode:
1050 1055 islink, isexec = gp.mode
1051 1056 dst = repo.wjoin(gp.path)
1052 1057 # patch won't create empty files
1053 1058 if gp.op == 'ADD' and not os.path.exists(dst):
1054 1059 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1055 1060 repo.wwrite(gp.path, '', flags)
1056 1061 elif gp.op != 'DELETE':
1057 1062 util.set_flags(dst, islink, isexec)
1058 1063 cmdutil.addremove(repo, cfiles, similarity=similarity)
1059 1064 files = patches.keys()
1060 1065 files.extend([r for r in removes if r not in files])
1061 1066 return sorted(files)
1062 1067
1063 1068 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1064 1069 """use <patcher> to apply <patchname> to the working directory.
1065 1070 returns whether patch was applied with fuzz factor."""
1066 1071
1067 1072 fuzz = False
1068 1073 if cwd:
1069 1074 args.append('-d %s' % util.shellquote(cwd))
1070 1075 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1071 1076 util.shellquote(patchname)))
1072 1077
1073 1078 for line in fp:
1074 1079 line = line.rstrip()
1075 1080 ui.note(line + '\n')
1076 1081 if line.startswith('patching file '):
1077 1082 pf = util.parse_patch_output(line)
1078 1083 printed_file = False
1079 1084 files.setdefault(pf, None)
1080 1085 elif line.find('with fuzz') >= 0:
1081 1086 fuzz = True
1082 1087 if not printed_file:
1083 1088 ui.warn(pf + '\n')
1084 1089 printed_file = True
1085 1090 ui.warn(line + '\n')
1086 1091 elif line.find('saving rejects to file') >= 0:
1087 1092 ui.warn(line + '\n')
1088 1093 elif line.find('FAILED') >= 0:
1089 1094 if not printed_file:
1090 1095 ui.warn(pf + '\n')
1091 1096 printed_file = True
1092 1097 ui.warn(line + '\n')
1093 1098 code = fp.close()
1094 1099 if code:
1095 1100 raise PatchError(_("patch command failed: %s") %
1096 1101 util.explain_exit(code)[0])
1097 1102 return fuzz
1098 1103
1099 1104 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1100 1105 """use builtin patch to apply <patchobj> to the working directory.
1101 1106 returns whether patch was applied with fuzz factor."""
1102 1107
1103 1108 if files is None:
1104 1109 files = {}
1105 1110 if eolmode is None:
1106 1111 eolmode = ui.config('patch', 'eol', 'strict')
1107 1112 try:
1108 1113 eol = {'strict': None, 'crlf': '\r\n', 'lf': '\n'}[eolmode.lower()]
1109 1114 except KeyError:
1110 1115 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1111 1116
1112 1117 try:
1113 1118 fp = open(patchobj, 'rb')
1114 1119 except TypeError:
1115 1120 fp = patchobj
1116 1121 if cwd:
1117 1122 curdir = os.getcwd()
1118 1123 os.chdir(cwd)
1119 1124 try:
1120 1125 ret = applydiff(ui, fp, files, strip=strip, eol=eol)
1121 1126 finally:
1122 1127 if cwd:
1123 1128 os.chdir(curdir)
1124 1129 if ret < 0:
1125 1130 raise PatchError
1126 1131 return ret > 0
1127 1132
1128 1133 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1129 1134 """Apply <patchname> to the working directory.
1130 1135
1131 1136 'eolmode' specifies how end of lines should be handled. It can be:
1132 1137 - 'strict': inputs are read in binary mode, EOLs are preserved
1133 1138 - 'crlf': EOLs are ignored when patching and reset to CRLF
1134 1139 - 'lf': EOLs are ignored when patching and reset to LF
1135 1140 - None: get it from user settings, default to 'strict'
1136 1141 'eolmode' is ignored when using an external patcher program.
1137 1142
1138 1143 Returns whether patch was applied with fuzz factor.
1139 1144 """
1140 1145 patcher = ui.config('ui', 'patch')
1141 1146 args = []
1142 1147 if files is None:
1143 1148 files = {}
1144 1149 try:
1145 1150 if patcher:
1146 1151 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1147 1152 files)
1148 1153 else:
1149 1154 try:
1150 1155 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1151 1156 except NoHunks:
1152 1157 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch'
1153 1158 ui.debug('no valid hunks found; trying with %r instead\n' %
1154 1159 patcher)
1155 1160 if util.needbinarypatch():
1156 1161 args.append('--binary')
1157 1162 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1158 1163 files)
1159 1164 except PatchError, err:
1160 1165 s = str(err)
1161 1166 if s:
1162 1167 raise util.Abort(s)
1163 1168 else:
1164 1169 raise util.Abort(_('patch failed to apply'))
1165 1170
1166 1171 def b85diff(to, tn):
1167 1172 '''print base85-encoded binary diff'''
1168 1173 def gitindex(text):
1169 1174 if not text:
1170 1175 return '0' * 40
1171 1176 l = len(text)
1172 1177 s = util.sha1('blob %d\0' % l)
1173 1178 s.update(text)
1174 1179 return s.hexdigest()
1175 1180
1176 1181 def fmtline(line):
1177 1182 l = len(line)
1178 1183 if l <= 26:
1179 1184 l = chr(ord('A') + l - 1)
1180 1185 else:
1181 1186 l = chr(l - 26 + ord('a') - 1)
1182 1187 return '%c%s\n' % (l, base85.b85encode(line, True))
1183 1188
1184 1189 def chunk(text, csize=52):
1185 1190 l = len(text)
1186 1191 i = 0
1187 1192 while i < l:
1188 1193 yield text[i:i+csize]
1189 1194 i += csize
1190 1195
1191 1196 tohash = gitindex(to)
1192 1197 tnhash = gitindex(tn)
1193 1198 if tohash == tnhash:
1194 1199 return ""
1195 1200
1196 1201 # TODO: deltas
1197 1202 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1198 1203 (tohash, tnhash, len(tn))]
1199 1204 for l in chunk(zlib.compress(tn)):
1200 1205 ret.append(fmtline(l))
1201 1206 ret.append('\n')
1202 1207 return ''.join(ret)
1203 1208
1204 1209 def _addmodehdr(header, omode, nmode):
1205 1210 if omode != nmode:
1206 1211 header.append('old mode %s\n' % omode)
1207 1212 header.append('new mode %s\n' % nmode)
1208 1213
1209 1214 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None):
1210 1215 '''yields diff of changes to files between two nodes, or node and
1211 1216 working directory.
1212 1217
1213 1218 if node1 is None, use first dirstate parent instead.
1214 1219 if node2 is None, compare node1 with working directory.'''
1215 1220
1216 1221 if opts is None:
1217 1222 opts = mdiff.defaultopts
1218 1223
1219 1224 if not node1 and not node2:
1220 1225 node1 = repo.dirstate.parents()[0]
1221 1226
1222 1227 def lrugetfilectx():
1223 1228 cache = {}
1224 1229 order = []
1225 1230 def getfilectx(f, ctx):
1226 1231 fctx = ctx.filectx(f, filelog=cache.get(f))
1227 1232 if f not in cache:
1228 1233 if len(cache) > 20:
1229 1234 del cache[order.pop(0)]
1230 1235 cache[f] = fctx.filelog()
1231 1236 else:
1232 1237 order.remove(f)
1233 1238 order.append(f)
1234 1239 return fctx
1235 1240 return getfilectx
1236 1241 getfilectx = lrugetfilectx()
1237 1242
1238 1243 ctx1 = repo[node1]
1239 1244 ctx2 = repo[node2]
1240 1245
1241 1246 if not changes:
1242 1247 changes = repo.status(ctx1, ctx2, match=match)
1243 1248 modified, added, removed = changes[:3]
1244 1249
1245 1250 if not modified and not added and not removed:
1246 1251 return
1247 1252
1248 1253 date1 = util.datestr(ctx1.date())
1249 1254 man1 = ctx1.manifest()
1250 1255
1251 1256 if repo.ui.quiet:
1252 1257 r = None
1253 1258 else:
1254 1259 hexfunc = repo.ui.debugflag and hex or short
1255 1260 r = [hexfunc(node) for node in [node1, node2] if node]
1256 1261
1257 1262 if opts.git:
1258 1263 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
1259 1264 copy = copy.copy()
1260 1265 for k, v in copy.items():
1261 1266 copy[v] = k
1262 1267
1263 1268 gone = set()
1264 1269 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1265 1270
1266 1271 for f in sorted(modified + added + removed):
1267 1272 to = None
1268 1273 tn = None
1269 1274 dodiff = True
1270 1275 header = []
1271 1276 if f in man1:
1272 1277 to = getfilectx(f, ctx1).data()
1273 1278 if f not in removed:
1274 1279 tn = getfilectx(f, ctx2).data()
1275 1280 a, b = f, f
1276 1281 if opts.git:
1277 1282 if f in added:
1278 1283 mode = gitmode[ctx2.flags(f)]
1279 1284 if f in copy:
1280 1285 a = copy[f]
1281 1286 omode = gitmode[man1.flags(a)]
1282 1287 _addmodehdr(header, omode, mode)
1283 1288 if a in removed and a not in gone:
1284 1289 op = 'rename'
1285 1290 gone.add(a)
1286 1291 else:
1287 1292 op = 'copy'
1288 1293 header.append('%s from %s\n' % (op, a))
1289 1294 header.append('%s to %s\n' % (op, f))
1290 1295 to = getfilectx(a, ctx1).data()
1291 1296 else:
1292 1297 header.append('new file mode %s\n' % mode)
1293 1298 if util.binary(tn):
1294 1299 dodiff = 'binary'
1295 1300 elif f in removed:
1296 1301 # have we already reported a copy above?
1297 1302 if f in copy and copy[f] in added and copy[copy[f]] == f:
1298 1303 dodiff = False
1299 1304 else:
1300 1305 header.append('deleted file mode %s\n' %
1301 1306 gitmode[man1.flags(f)])
1302 1307 else:
1303 1308 omode = gitmode[man1.flags(f)]
1304 1309 nmode = gitmode[ctx2.flags(f)]
1305 1310 _addmodehdr(header, omode, nmode)
1306 1311 if util.binary(to) or util.binary(tn):
1307 1312 dodiff = 'binary'
1308 1313 r = None
1309 1314 header.insert(0, mdiff.diffline(r, a, b, opts))
1310 1315 if dodiff:
1311 1316 if dodiff == 'binary':
1312 1317 text = b85diff(to, tn)
1313 1318 else:
1314 1319 text = mdiff.unidiff(to, date1,
1315 1320 # ctx2 date may be dynamic
1316 1321 tn, util.datestr(ctx2.date()),
1317 1322 a, b, r, opts=opts)
1318 1323 if header and (text or len(header) > 1):
1319 1324 yield ''.join(header)
1320 1325 if text:
1321 1326 yield text
1322 1327
1323 1328 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1324 1329 opts=None):
1325 1330 '''export changesets as hg patches.'''
1326 1331
1327 1332 total = len(revs)
1328 1333 revwidth = max([len(str(rev)) for rev in revs])
1329 1334
1330 1335 def single(rev, seqno, fp):
1331 1336 ctx = repo[rev]
1332 1337 node = ctx.node()
1333 1338 parents = [p.node() for p in ctx.parents() if p]
1334 1339 branch = ctx.branch()
1335 1340 if switch_parent:
1336 1341 parents.reverse()
1337 1342 prev = (parents and parents[0]) or nullid
1338 1343
1339 1344 if not fp:
1340 1345 fp = cmdutil.make_file(repo, template, node, total=total,
1341 1346 seqno=seqno, revwidth=revwidth,
1342 1347 mode='ab')
1343 1348 if fp != sys.stdout and hasattr(fp, 'name'):
1344 1349 repo.ui.note("%s\n" % fp.name)
1345 1350
1346 1351 fp.write("# HG changeset patch\n")
1347 1352 fp.write("# User %s\n" % ctx.user())
1348 1353 fp.write("# Date %d %d\n" % ctx.date())
1349 1354 if branch and (branch != 'default'):
1350 1355 fp.write("# Branch %s\n" % branch)
1351 1356 fp.write("# Node ID %s\n" % hex(node))
1352 1357 fp.write("# Parent %s\n" % hex(prev))
1353 1358 if len(parents) > 1:
1354 1359 fp.write("# Parent %s\n" % hex(parents[1]))
1355 1360 fp.write(ctx.description().rstrip())
1356 1361 fp.write("\n\n")
1357 1362
1358 1363 for chunk in diff(repo, prev, node, opts=opts):
1359 1364 fp.write(chunk)
1360 1365
1361 1366 for seqno, rev in enumerate(revs):
1362 1367 single(rev, seqno+1, fp)
1363 1368
1364 1369 def diffstatdata(lines):
1365 1370 filename, adds, removes = None, 0, 0
1366 1371 for line in lines:
1367 1372 if line.startswith('diff'):
1368 1373 if filename:
1369 1374 isbinary = adds == 0 and removes == 0
1370 1375 yield (filename, adds, removes, isbinary)
1371 1376 # set numbers to 0 anyway when starting new file
1372 1377 adds, removes = 0, 0
1373 1378 if line.startswith('diff --git'):
1374 1379 filename = gitre.search(line).group(1)
1375 1380 else:
1376 1381 # format: "diff -r ... -r ... filename"
1377 1382 filename = line.split(None, 5)[-1]
1378 1383 elif line.startswith('+') and not line.startswith('+++'):
1379 1384 adds += 1
1380 1385 elif line.startswith('-') and not line.startswith('---'):
1381 1386 removes += 1
1382 1387 if filename:
1383 1388 isbinary = adds == 0 and removes == 0
1384 1389 yield (filename, adds, removes, isbinary)
1385 1390
1386 1391 def diffstat(lines, width=80, git=False):
1387 1392 output = []
1388 1393 stats = list(diffstatdata(lines))
1389 1394
1390 1395 maxtotal, maxname = 0, 0
1391 1396 totaladds, totalremoves = 0, 0
1392 1397 hasbinary = False
1393 1398 for filename, adds, removes, isbinary in stats:
1394 1399 totaladds += adds
1395 1400 totalremoves += removes
1396 1401 maxname = max(maxname, len(filename))
1397 1402 maxtotal = max(maxtotal, adds+removes)
1398 1403 if isbinary:
1399 1404 hasbinary = True
1400 1405
1401 1406 countwidth = len(str(maxtotal))
1402 1407 if hasbinary and countwidth < 3:
1403 1408 countwidth = 3
1404 1409 graphwidth = width - countwidth - maxname - 6
1405 1410 if graphwidth < 10:
1406 1411 graphwidth = 10
1407 1412
1408 1413 def scale(i):
1409 1414 if maxtotal <= graphwidth:
1410 1415 return i
1411 1416 # If diffstat runs out of room it doesn't print anything,
1412 1417 # which isn't very useful, so always print at least one + or -
1413 1418 # if there were at least some changes.
1414 1419 return max(i * graphwidth // maxtotal, int(bool(i)))
1415 1420
1416 1421 for filename, adds, removes, isbinary in stats:
1417 1422 if git and isbinary:
1418 1423 count = 'Bin'
1419 1424 else:
1420 1425 count = adds + removes
1421 1426 pluses = '+' * scale(adds)
1422 1427 minuses = '-' * scale(removes)
1423 1428 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1424 1429 count, pluses, minuses))
1425 1430
1426 1431 if stats:
1427 1432 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1428 1433 % (len(stats), totaladds, totalremoves))
1429 1434
1430 1435 return ''.join(output)
General Comments 0
You need to be logged in to leave comments. Login now