##// END OF EJS Templates
Make hg diff --git -r revA:revB detect (inverted) copies if revA > revB
Alexis S. L. Carvalho -
r5264:0fc16031 default
parent child Browse files
Show More
@@ -1,1330 +1,1347 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
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 from i18n import _
10 10 from node import *
11 11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
12 12 import cStringIO, email.Parser, os, popen2, re, sha
13 13 import sys, tempfile, zlib
14 14
15 15 class PatchError(Exception):
16 16 pass
17 17
18 18 class NoHunks(PatchError):
19 19 pass
20 20
21 21 # helper functions
22 22
23 23 def copyfile(src, dst, basedir=None):
24 24 if not basedir:
25 25 basedir = os.getcwd()
26 26
27 27 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
28 28 if os.path.exists(absdst):
29 29 raise util.Abort(_("cannot create %s: destination already exists") %
30 30 dst)
31 31
32 32 targetdir = os.path.dirname(absdst)
33 33 if not os.path.isdir(targetdir):
34 34 os.makedirs(targetdir)
35 35
36 36 util.copyfile(abssrc, absdst)
37 37
38 38 # public functions
39 39
40 40 def extract(ui, fileobj):
41 41 '''extract patch from data read from fileobj.
42 42
43 43 patch can be a normal patch or contained in an email message.
44 44
45 45 return tuple (filename, message, user, date, node, p1, p2).
46 46 Any item in the returned tuple can be None. If filename is None,
47 47 fileobj did not contain a patch. Caller must unlink filename when done.'''
48 48
49 49 # attempt to detect the start of a patch
50 50 # (this heuristic is borrowed from quilt)
51 51 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
52 52 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
53 53 '(---|\*\*\*)[ \t])', re.MULTILINE)
54 54
55 55 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
56 56 tmpfp = os.fdopen(fd, 'w')
57 57 try:
58 58 msg = email.Parser.Parser().parse(fileobj)
59 59
60 60 subject = msg['Subject']
61 61 user = msg['From']
62 62 # should try to parse msg['Date']
63 63 date = None
64 64 nodeid = None
65 65 branch = None
66 66 parents = []
67 67
68 68 if subject:
69 69 if subject.startswith('[PATCH'):
70 70 pend = subject.find(']')
71 71 if pend >= 0:
72 72 subject = subject[pend+1:].lstrip()
73 73 subject = subject.replace('\n\t', ' ')
74 74 ui.debug('Subject: %s\n' % subject)
75 75 if user:
76 76 ui.debug('From: %s\n' % user)
77 77 diffs_seen = 0
78 78 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
79 79 message = ''
80 80 for part in msg.walk():
81 81 content_type = part.get_content_type()
82 82 ui.debug('Content-Type: %s\n' % content_type)
83 83 if content_type not in ok_types:
84 84 continue
85 85 payload = part.get_payload(decode=True)
86 86 m = diffre.search(payload)
87 87 if m:
88 88 hgpatch = False
89 89 ignoretext = False
90 90
91 91 ui.debug(_('found patch at byte %d\n') % m.start(0))
92 92 diffs_seen += 1
93 93 cfp = cStringIO.StringIO()
94 94 for line in payload[:m.start(0)].splitlines():
95 95 if line.startswith('# HG changeset patch'):
96 96 ui.debug(_('patch generated by hg export\n'))
97 97 hgpatch = True
98 98 # drop earlier commit message content
99 99 cfp.seek(0)
100 100 cfp.truncate()
101 101 subject = None
102 102 elif hgpatch:
103 103 if line.startswith('# User '):
104 104 user = line[7:]
105 105 ui.debug('From: %s\n' % user)
106 106 elif line.startswith("# Date "):
107 107 date = line[7:]
108 108 elif line.startswith("# Branch "):
109 109 branch = line[9:]
110 110 elif line.startswith("# Node ID "):
111 111 nodeid = line[10:]
112 112 elif line.startswith("# Parent "):
113 113 parents.append(line[10:])
114 114 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
115 115 ignoretext = True
116 116 if not line.startswith('# ') and not ignoretext:
117 117 cfp.write(line)
118 118 cfp.write('\n')
119 119 message = cfp.getvalue()
120 120 if tmpfp:
121 121 tmpfp.write(payload)
122 122 if not payload.endswith('\n'):
123 123 tmpfp.write('\n')
124 124 elif not diffs_seen and message and content_type == 'text/plain':
125 125 message += '\n' + payload
126 126 except:
127 127 tmpfp.close()
128 128 os.unlink(tmpname)
129 129 raise
130 130
131 131 if subject and not message.startswith(subject):
132 132 message = '%s\n%s' % (subject, message)
133 133 tmpfp.close()
134 134 if not diffs_seen:
135 135 os.unlink(tmpname)
136 136 return None, message, user, date, branch, None, None, None
137 137 p1 = parents and parents.pop(0) or None
138 138 p2 = parents and parents.pop(0) or None
139 139 return tmpname, message, user, date, branch, nodeid, p1, p2
140 140
141 141 GP_PATCH = 1 << 0 # we have to run patch
142 142 GP_FILTER = 1 << 1 # there's some copy/rename operation
143 143 GP_BINARY = 1 << 2 # there's a binary patch
144 144
145 145 def readgitpatch(fp, firstline=None):
146 146 """extract git-style metadata about patches from <patchname>"""
147 147 class gitpatch:
148 148 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
149 149 def __init__(self, path):
150 150 self.path = path
151 151 self.oldpath = None
152 152 self.mode = None
153 153 self.op = 'MODIFY'
154 154 self.copymod = False
155 155 self.lineno = 0
156 156 self.binary = False
157 157
158 158 def reader(fp, firstline):
159 159 if firstline is not None:
160 160 yield firstline
161 161 for line in fp:
162 162 yield line
163 163
164 164 # Filter patch for git information
165 165 gitre = re.compile('diff --git a/(.*) b/(.*)')
166 166 gp = None
167 167 gitpatches = []
168 168 # Can have a git patch with only metadata, causing patch to complain
169 169 dopatch = 0
170 170
171 171 lineno = 0
172 172 for line in reader(fp, firstline):
173 173 lineno += 1
174 174 if line.startswith('diff --git'):
175 175 m = gitre.match(line)
176 176 if m:
177 177 if gp:
178 178 gitpatches.append(gp)
179 179 src, dst = m.group(1, 2)
180 180 gp = gitpatch(dst)
181 181 gp.lineno = lineno
182 182 elif gp:
183 183 if line.startswith('--- '):
184 184 if gp.op in ('COPY', 'RENAME'):
185 185 gp.copymod = True
186 186 dopatch |= GP_FILTER
187 187 gitpatches.append(gp)
188 188 gp = None
189 189 dopatch |= GP_PATCH
190 190 continue
191 191 if line.startswith('rename from '):
192 192 gp.op = 'RENAME'
193 193 gp.oldpath = line[12:].rstrip()
194 194 elif line.startswith('rename to '):
195 195 gp.path = line[10:].rstrip()
196 196 elif line.startswith('copy from '):
197 197 gp.op = 'COPY'
198 198 gp.oldpath = line[10:].rstrip()
199 199 elif line.startswith('copy to '):
200 200 gp.path = line[8:].rstrip()
201 201 elif line.startswith('deleted file'):
202 202 gp.op = 'DELETE'
203 203 elif line.startswith('new file mode '):
204 204 gp.op = 'ADD'
205 205 gp.mode = int(line.rstrip()[-6:], 8)
206 206 elif line.startswith('new mode '):
207 207 gp.mode = int(line.rstrip()[-6:], 8)
208 208 elif line.startswith('GIT binary patch'):
209 209 dopatch |= GP_BINARY
210 210 gp.binary = True
211 211 if gp:
212 212 gitpatches.append(gp)
213 213
214 214 if not gitpatches:
215 215 dopatch = GP_PATCH
216 216
217 217 return (dopatch, gitpatches)
218 218
219 219 def patch(patchname, ui, strip=1, cwd=None, files={}):
220 220 """apply <patchname> to the working directory.
221 221 returns whether patch was applied with fuzz factor."""
222 222 patcher = ui.config('ui', 'patch')
223 223 args = []
224 224 try:
225 225 if patcher:
226 226 return externalpatch(patcher, args, patchname, ui, strip, cwd,
227 227 files)
228 228 else:
229 229 try:
230 230 return internalpatch(patchname, ui, strip, cwd, files)
231 231 except NoHunks:
232 232 patcher = util.find_exe('gpatch') or util.find_exe('patch')
233 233 ui.debug('no valid hunks found; trying with %r instead\n' %
234 234 patcher)
235 235 if util.needbinarypatch():
236 236 args.append('--binary')
237 237 return externalpatch(patcher, args, patchname, ui, strip, cwd,
238 238 files)
239 239 except PatchError, err:
240 240 s = str(err)
241 241 if s:
242 242 raise util.Abort(s)
243 243 else:
244 244 raise util.Abort(_('patch failed to apply'))
245 245
246 246 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
247 247 """use <patcher> to apply <patchname> to the working directory.
248 248 returns whether patch was applied with fuzz factor."""
249 249
250 250 fuzz = False
251 251 if cwd:
252 252 args.append('-d %s' % util.shellquote(cwd))
253 253 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
254 254 util.shellquote(patchname)))
255 255
256 256 for line in fp:
257 257 line = line.rstrip()
258 258 ui.note(line + '\n')
259 259 if line.startswith('patching file '):
260 260 pf = util.parse_patch_output(line)
261 261 printed_file = False
262 262 files.setdefault(pf, (None, None))
263 263 elif line.find('with fuzz') >= 0:
264 264 fuzz = True
265 265 if not printed_file:
266 266 ui.warn(pf + '\n')
267 267 printed_file = True
268 268 ui.warn(line + '\n')
269 269 elif line.find('saving rejects to file') >= 0:
270 270 ui.warn(line + '\n')
271 271 elif line.find('FAILED') >= 0:
272 272 if not printed_file:
273 273 ui.warn(pf + '\n')
274 274 printed_file = True
275 275 ui.warn(line + '\n')
276 276 code = fp.close()
277 277 if code:
278 278 raise PatchError(_("patch command failed: %s") %
279 279 util.explain_exit(code)[0])
280 280 return fuzz
281 281
282 282 def internalpatch(patchobj, ui, strip, cwd, files={}):
283 283 """use builtin patch to apply <patchobj> to the working directory.
284 284 returns whether patch was applied with fuzz factor."""
285 285 try:
286 286 fp = file(patchobj, 'rb')
287 287 except TypeError:
288 288 fp = patchobj
289 289 if cwd:
290 290 curdir = os.getcwd()
291 291 os.chdir(cwd)
292 292 try:
293 293 ret = applydiff(ui, fp, files, strip=strip)
294 294 finally:
295 295 if cwd:
296 296 os.chdir(curdir)
297 297 if ret < 0:
298 298 raise PatchError
299 299 return ret > 0
300 300
301 301 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
302 302 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
303 303 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
304 304
305 305 class patchfile:
306 306 def __init__(self, ui, fname):
307 307 self.fname = fname
308 308 self.ui = ui
309 309 try:
310 310 fp = file(fname, 'rb')
311 311 self.lines = fp.readlines()
312 312 self.exists = True
313 313 except IOError:
314 314 dirname = os.path.dirname(fname)
315 315 if dirname and not os.path.isdir(dirname):
316 316 dirs = dirname.split(os.path.sep)
317 317 d = ""
318 318 for x in dirs:
319 319 d = os.path.join(d, x)
320 320 if not os.path.isdir(d):
321 321 os.mkdir(d)
322 322 self.lines = []
323 323 self.exists = False
324 324
325 325 self.hash = {}
326 326 self.dirty = 0
327 327 self.offset = 0
328 328 self.rej = []
329 329 self.fileprinted = False
330 330 self.printfile(False)
331 331 self.hunks = 0
332 332
333 333 def printfile(self, warn):
334 334 if self.fileprinted:
335 335 return
336 336 if warn or self.ui.verbose:
337 337 self.fileprinted = True
338 338 s = _("patching file %s\n") % self.fname
339 339 if warn:
340 340 self.ui.warn(s)
341 341 else:
342 342 self.ui.note(s)
343 343
344 344
345 345 def findlines(self, l, linenum):
346 346 # looks through the hash and finds candidate lines. The
347 347 # result is a list of line numbers sorted based on distance
348 348 # from linenum
349 349 def sorter(a, b):
350 350 vala = abs(a - linenum)
351 351 valb = abs(b - linenum)
352 352 return cmp(vala, valb)
353 353
354 354 try:
355 355 cand = self.hash[l]
356 356 except:
357 357 return []
358 358
359 359 if len(cand) > 1:
360 360 # resort our list of potentials forward then back.
361 361 cand.sort(cmp=sorter)
362 362 return cand
363 363
364 364 def hashlines(self):
365 365 self.hash = {}
366 366 for x in xrange(len(self.lines)):
367 367 s = self.lines[x]
368 368 self.hash.setdefault(s, []).append(x)
369 369
370 370 def write_rej(self):
371 371 # our rejects are a little different from patch(1). This always
372 372 # creates rejects in the same form as the original patch. A file
373 373 # header is inserted so that you can run the reject through patch again
374 374 # without having to type the filename.
375 375
376 376 if not self.rej:
377 377 return
378 378 if self.hunks != 1:
379 379 hunkstr = "s"
380 380 else:
381 381 hunkstr = ""
382 382
383 383 fname = self.fname + ".rej"
384 384 self.ui.warn(
385 385 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") %
386 386 (len(self.rej), self.hunks, hunkstr, fname))
387 387 try: os.unlink(fname)
388 388 except:
389 389 pass
390 390 fp = file(fname, 'wb')
391 391 base = os.path.basename(self.fname)
392 392 fp.write("--- %s\n+++ %s\n" % (base, base))
393 393 for x in self.rej:
394 394 for l in x.hunk:
395 395 fp.write(l)
396 396 if l[-1] != '\n':
397 397 fp.write("\n\ No newline at end of file\n")
398 398
399 399 def write(self, dest=None):
400 400 if self.dirty:
401 401 if not dest:
402 402 dest = self.fname
403 403 st = None
404 404 try:
405 405 st = os.lstat(dest)
406 406 if st.st_nlink > 1:
407 407 os.unlink(dest)
408 408 except: pass
409 409 fp = file(dest, 'wb')
410 410 if st:
411 411 os.chmod(dest, st.st_mode)
412 412 fp.writelines(self.lines)
413 413 fp.close()
414 414
415 415 def close(self):
416 416 self.write()
417 417 self.write_rej()
418 418
419 419 def apply(self, h, reverse):
420 420 if not h.complete():
421 421 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
422 422 (h.number, h.desc, len(h.a), h.lena, len(h.b),
423 423 h.lenb))
424 424
425 425 self.hunks += 1
426 426 if reverse:
427 427 h.reverse()
428 428
429 429 if self.exists and h.createfile():
430 430 self.ui.warn(_("file %s already exists\n") % self.fname)
431 431 self.rej.append(h)
432 432 return -1
433 433
434 434 if isinstance(h, binhunk):
435 435 if h.rmfile():
436 436 os.unlink(self.fname)
437 437 else:
438 438 self.lines[:] = h.new()
439 439 self.offset += len(h.new())
440 440 self.dirty = 1
441 441 return 0
442 442
443 443 # fast case first, no offsets, no fuzz
444 444 old = h.old()
445 445 # patch starts counting at 1 unless we are adding the file
446 446 if h.starta == 0:
447 447 start = 0
448 448 else:
449 449 start = h.starta + self.offset - 1
450 450 orig_start = start
451 451 if diffhelpers.testhunk(old, self.lines, start) == 0:
452 452 if h.rmfile():
453 453 os.unlink(self.fname)
454 454 else:
455 455 self.lines[start : start + h.lena] = h.new()
456 456 self.offset += h.lenb - h.lena
457 457 self.dirty = 1
458 458 return 0
459 459
460 460 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
461 461 self.hashlines()
462 462 if h.hunk[-1][0] != ' ':
463 463 # if the hunk tried to put something at the bottom of the file
464 464 # override the start line and use eof here
465 465 search_start = len(self.lines)
466 466 else:
467 467 search_start = orig_start
468 468
469 469 for fuzzlen in xrange(3):
470 470 for toponly in [ True, False ]:
471 471 old = h.old(fuzzlen, toponly)
472 472
473 473 cand = self.findlines(old[0][1:], search_start)
474 474 for l in cand:
475 475 if diffhelpers.testhunk(old, self.lines, l) == 0:
476 476 newlines = h.new(fuzzlen, toponly)
477 477 self.lines[l : l + len(old)] = newlines
478 478 self.offset += len(newlines) - len(old)
479 479 self.dirty = 1
480 480 if fuzzlen:
481 481 fuzzstr = "with fuzz %d " % fuzzlen
482 482 f = self.ui.warn
483 483 self.printfile(True)
484 484 else:
485 485 fuzzstr = ""
486 486 f = self.ui.note
487 487 offset = l - orig_start - fuzzlen
488 488 if offset == 1:
489 489 linestr = "line"
490 490 else:
491 491 linestr = "lines"
492 492 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
493 493 (h.number, l+1, fuzzstr, offset, linestr))
494 494 return fuzzlen
495 495 self.printfile(True)
496 496 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
497 497 self.rej.append(h)
498 498 return -1
499 499
500 500 class hunk:
501 501 def __init__(self, desc, num, lr, context):
502 502 self.number = num
503 503 self.desc = desc
504 504 self.hunk = [ desc ]
505 505 self.a = []
506 506 self.b = []
507 507 if context:
508 508 self.read_context_hunk(lr)
509 509 else:
510 510 self.read_unified_hunk(lr)
511 511
512 512 def read_unified_hunk(self, lr):
513 513 m = unidesc.match(self.desc)
514 514 if not m:
515 515 raise PatchError(_("bad hunk #%d") % self.number)
516 516 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
517 517 if self.lena == None:
518 518 self.lena = 1
519 519 else:
520 520 self.lena = int(self.lena)
521 521 if self.lenb == None:
522 522 self.lenb = 1
523 523 else:
524 524 self.lenb = int(self.lenb)
525 525 self.starta = int(self.starta)
526 526 self.startb = int(self.startb)
527 527 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
528 528 # if we hit eof before finishing out the hunk, the last line will
529 529 # be zero length. Lets try to fix it up.
530 530 while len(self.hunk[-1]) == 0:
531 531 del self.hunk[-1]
532 532 del self.a[-1]
533 533 del self.b[-1]
534 534 self.lena -= 1
535 535 self.lenb -= 1
536 536
537 537 def read_context_hunk(self, lr):
538 538 self.desc = lr.readline()
539 539 m = contextdesc.match(self.desc)
540 540 if not m:
541 541 raise PatchError(_("bad hunk #%d") % self.number)
542 542 foo, self.starta, foo2, aend, foo3 = m.groups()
543 543 self.starta = int(self.starta)
544 544 if aend == None:
545 545 aend = self.starta
546 546 self.lena = int(aend) - self.starta
547 547 if self.starta:
548 548 self.lena += 1
549 549 for x in xrange(self.lena):
550 550 l = lr.readline()
551 551 if l.startswith('---'):
552 552 lr.push(l)
553 553 break
554 554 s = l[2:]
555 555 if l.startswith('- ') or l.startswith('! '):
556 556 u = '-' + s
557 557 elif l.startswith(' '):
558 558 u = ' ' + s
559 559 else:
560 560 raise PatchError(_("bad hunk #%d old text line %d") %
561 561 (self.number, x))
562 562 self.a.append(u)
563 563 self.hunk.append(u)
564 564
565 565 l = lr.readline()
566 566 if l.startswith('\ '):
567 567 s = self.a[-1][:-1]
568 568 self.a[-1] = s
569 569 self.hunk[-1] = s
570 570 l = lr.readline()
571 571 m = contextdesc.match(l)
572 572 if not m:
573 573 raise PatchError(_("bad hunk #%d") % self.number)
574 574 foo, self.startb, foo2, bend, foo3 = m.groups()
575 575 self.startb = int(self.startb)
576 576 if bend == None:
577 577 bend = self.startb
578 578 self.lenb = int(bend) - self.startb
579 579 if self.startb:
580 580 self.lenb += 1
581 581 hunki = 1
582 582 for x in xrange(self.lenb):
583 583 l = lr.readline()
584 584 if l.startswith('\ '):
585 585 s = self.b[-1][:-1]
586 586 self.b[-1] = s
587 587 self.hunk[hunki-1] = s
588 588 continue
589 589 if not l:
590 590 lr.push(l)
591 591 break
592 592 s = l[2:]
593 593 if l.startswith('+ ') or l.startswith('! '):
594 594 u = '+' + s
595 595 elif l.startswith(' '):
596 596 u = ' ' + s
597 597 elif len(self.b) == 0:
598 598 # this can happen when the hunk does not add any lines
599 599 lr.push(l)
600 600 break
601 601 else:
602 602 raise PatchError(_("bad hunk #%d old text line %d") %
603 603 (self.number, x))
604 604 self.b.append(s)
605 605 while True:
606 606 if hunki >= len(self.hunk):
607 607 h = ""
608 608 else:
609 609 h = self.hunk[hunki]
610 610 hunki += 1
611 611 if h == u:
612 612 break
613 613 elif h.startswith('-'):
614 614 continue
615 615 else:
616 616 self.hunk.insert(hunki-1, u)
617 617 break
618 618
619 619 if not self.a:
620 620 # this happens when lines were only added to the hunk
621 621 for x in self.hunk:
622 622 if x.startswith('-') or x.startswith(' '):
623 623 self.a.append(x)
624 624 if not self.b:
625 625 # this happens when lines were only deleted from the hunk
626 626 for x in self.hunk:
627 627 if x.startswith('+') or x.startswith(' '):
628 628 self.b.append(x[1:])
629 629 # @@ -start,len +start,len @@
630 630 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
631 631 self.startb, self.lenb)
632 632 self.hunk[0] = self.desc
633 633
634 634 def reverse(self):
635 635 origlena = self.lena
636 636 origstarta = self.starta
637 637 self.lena = self.lenb
638 638 self.starta = self.startb
639 639 self.lenb = origlena
640 640 self.startb = origstarta
641 641 self.a = []
642 642 self.b = []
643 643 # self.hunk[0] is the @@ description
644 644 for x in xrange(1, len(self.hunk)):
645 645 o = self.hunk[x]
646 646 if o.startswith('-'):
647 647 n = '+' + o[1:]
648 648 self.b.append(o[1:])
649 649 elif o.startswith('+'):
650 650 n = '-' + o[1:]
651 651 self.a.append(n)
652 652 else:
653 653 n = o
654 654 self.b.append(o[1:])
655 655 self.a.append(o)
656 656 self.hunk[x] = o
657 657
658 658 def fix_newline(self):
659 659 diffhelpers.fix_newline(self.hunk, self.a, self.b)
660 660
661 661 def complete(self):
662 662 return len(self.a) == self.lena and len(self.b) == self.lenb
663 663
664 664 def createfile(self):
665 665 return self.starta == 0 and self.lena == 0
666 666
667 667 def rmfile(self):
668 668 return self.startb == 0 and self.lenb == 0
669 669
670 670 def fuzzit(self, l, fuzz, toponly):
671 671 # this removes context lines from the top and bottom of list 'l'. It
672 672 # checks the hunk to make sure only context lines are removed, and then
673 673 # returns a new shortened list of lines.
674 674 fuzz = min(fuzz, len(l)-1)
675 675 if fuzz:
676 676 top = 0
677 677 bot = 0
678 678 hlen = len(self.hunk)
679 679 for x in xrange(hlen-1):
680 680 # the hunk starts with the @@ line, so use x+1
681 681 if self.hunk[x+1][0] == ' ':
682 682 top += 1
683 683 else:
684 684 break
685 685 if not toponly:
686 686 for x in xrange(hlen-1):
687 687 if self.hunk[hlen-bot-1][0] == ' ':
688 688 bot += 1
689 689 else:
690 690 break
691 691
692 692 # top and bot now count context in the hunk
693 693 # adjust them if either one is short
694 694 context = max(top, bot, 3)
695 695 if bot < context:
696 696 bot = max(0, fuzz - (context - bot))
697 697 else:
698 698 bot = min(fuzz, bot)
699 699 if top < context:
700 700 top = max(0, fuzz - (context - top))
701 701 else:
702 702 top = min(fuzz, top)
703 703
704 704 return l[top:len(l)-bot]
705 705 return l
706 706
707 707 def old(self, fuzz=0, toponly=False):
708 708 return self.fuzzit(self.a, fuzz, toponly)
709 709
710 710 def newctrl(self):
711 711 res = []
712 712 for x in self.hunk:
713 713 c = x[0]
714 714 if c == ' ' or c == '+':
715 715 res.append(x)
716 716 return res
717 717
718 718 def new(self, fuzz=0, toponly=False):
719 719 return self.fuzzit(self.b, fuzz, toponly)
720 720
721 721 class binhunk:
722 722 'A binary patch file. Only understands literals so far.'
723 723 def __init__(self, gitpatch):
724 724 self.gitpatch = gitpatch
725 725 self.text = None
726 726 self.hunk = ['GIT binary patch\n']
727 727
728 728 def createfile(self):
729 729 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
730 730
731 731 def rmfile(self):
732 732 return self.gitpatch.op == 'DELETE'
733 733
734 734 def complete(self):
735 735 return self.text is not None
736 736
737 737 def new(self):
738 738 return [self.text]
739 739
740 740 def extract(self, fp):
741 741 line = fp.readline()
742 742 self.hunk.append(line)
743 743 while line and not line.startswith('literal '):
744 744 line = fp.readline()
745 745 self.hunk.append(line)
746 746 if not line:
747 747 raise PatchError(_('could not extract binary patch'))
748 748 size = int(line[8:].rstrip())
749 749 dec = []
750 750 line = fp.readline()
751 751 self.hunk.append(line)
752 752 while len(line) > 1:
753 753 l = line[0]
754 754 if l <= 'Z' and l >= 'A':
755 755 l = ord(l) - ord('A') + 1
756 756 else:
757 757 l = ord(l) - ord('a') + 27
758 758 dec.append(base85.b85decode(line[1:-1])[:l])
759 759 line = fp.readline()
760 760 self.hunk.append(line)
761 761 text = zlib.decompress(''.join(dec))
762 762 if len(text) != size:
763 763 raise PatchError(_('binary patch is %d bytes, not %d') %
764 764 len(text), size)
765 765 self.text = text
766 766
767 767 def parsefilename(str):
768 768 # --- filename \t|space stuff
769 769 s = str[4:]
770 770 i = s.find('\t')
771 771 if i < 0:
772 772 i = s.find(' ')
773 773 if i < 0:
774 774 return s
775 775 return s[:i]
776 776
777 777 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
778 778 def pathstrip(path, count=1):
779 779 pathlen = len(path)
780 780 i = 0
781 781 if count == 0:
782 782 return path.rstrip()
783 783 while count > 0:
784 784 i = path.find('/', i)
785 785 if i == -1:
786 786 raise PatchError(_("unable to strip away %d dirs from %s") %
787 787 (count, path))
788 788 i += 1
789 789 # consume '//' in the path
790 790 while i < pathlen - 1 and path[i] == '/':
791 791 i += 1
792 792 count -= 1
793 793 return path[i:].rstrip()
794 794
795 795 nulla = afile_orig == "/dev/null"
796 796 nullb = bfile_orig == "/dev/null"
797 797 afile = pathstrip(afile_orig, strip)
798 798 gooda = os.path.exists(afile) and not nulla
799 799 bfile = pathstrip(bfile_orig, strip)
800 800 if afile == bfile:
801 801 goodb = gooda
802 802 else:
803 803 goodb = os.path.exists(bfile) and not nullb
804 804 createfunc = hunk.createfile
805 805 if reverse:
806 806 createfunc = hunk.rmfile
807 807 if not goodb and not gooda and not createfunc():
808 808 raise PatchError(_("unable to find %s or %s for patching") %
809 809 (afile, bfile))
810 810 if gooda and goodb:
811 811 fname = bfile
812 812 if afile in bfile:
813 813 fname = afile
814 814 elif gooda:
815 815 fname = afile
816 816 elif not nullb:
817 817 fname = bfile
818 818 if afile in bfile:
819 819 fname = afile
820 820 elif not nulla:
821 821 fname = afile
822 822 return fname
823 823
824 824 class linereader:
825 825 # simple class to allow pushing lines back into the input stream
826 826 def __init__(self, fp):
827 827 self.fp = fp
828 828 self.buf = []
829 829
830 830 def push(self, line):
831 831 self.buf.append(line)
832 832
833 833 def readline(self):
834 834 if self.buf:
835 835 l = self.buf[0]
836 836 del self.buf[0]
837 837 return l
838 838 return self.fp.readline()
839 839
840 840 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
841 841 rejmerge=None, updatedir=None):
842 842 """reads a patch from fp and tries to apply it. The dict 'changed' is
843 843 filled in with all of the filenames changed by the patch. Returns 0
844 844 for a clean patch, -1 if any rejects were found and 1 if there was
845 845 any fuzz."""
846 846
847 847 def scangitpatch(fp, firstline, cwd=None):
848 848 '''git patches can modify a file, then copy that file to
849 849 a new file, but expect the source to be the unmodified form.
850 850 So we scan the patch looking for that case so we can do
851 851 the copies ahead of time.'''
852 852
853 853 pos = 0
854 854 try:
855 855 pos = fp.tell()
856 856 except IOError:
857 857 fp = cStringIO.StringIO(fp.read())
858 858
859 859 (dopatch, gitpatches) = readgitpatch(fp, firstline)
860 860 for gp in gitpatches:
861 861 if gp.copymod:
862 862 copyfile(gp.oldpath, gp.path, basedir=cwd)
863 863
864 864 fp.seek(pos)
865 865
866 866 return fp, dopatch, gitpatches
867 867
868 868 current_hunk = None
869 869 current_file = None
870 870 afile = ""
871 871 bfile = ""
872 872 state = None
873 873 hunknum = 0
874 874 rejects = 0
875 875
876 876 git = False
877 877 gitre = re.compile('diff --git (a/.*) (b/.*)')
878 878
879 879 # our states
880 880 BFILE = 1
881 881 err = 0
882 882 context = None
883 883 lr = linereader(fp)
884 884 dopatch = True
885 885 gitworkdone = False
886 886
887 887 while True:
888 888 newfile = False
889 889 x = lr.readline()
890 890 if not x:
891 891 break
892 892 if current_hunk:
893 893 if x.startswith('\ '):
894 894 current_hunk.fix_newline()
895 895 ret = current_file.apply(current_hunk, reverse)
896 896 if ret >= 0:
897 897 changed.setdefault(current_file.fname, (None, None))
898 898 if ret > 0:
899 899 err = 1
900 900 current_hunk = None
901 901 gitworkdone = False
902 902 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
903 903 ((context or context == None) and x.startswith('***************')))):
904 904 try:
905 905 if context == None and x.startswith('***************'):
906 906 context = True
907 907 current_hunk = hunk(x, hunknum + 1, lr, context)
908 908 except PatchError, err:
909 909 ui.debug(err)
910 910 current_hunk = None
911 911 continue
912 912 hunknum += 1
913 913 if not current_file:
914 914 if sourcefile:
915 915 current_file = patchfile(ui, sourcefile)
916 916 else:
917 917 current_file = selectfile(afile, bfile, current_hunk,
918 918 strip, reverse)
919 919 current_file = patchfile(ui, current_file)
920 920 elif state == BFILE and x.startswith('GIT binary patch'):
921 921 current_hunk = binhunk(changed[bfile[2:]][1])
922 922 if not current_file:
923 923 if sourcefile:
924 924 current_file = patchfile(ui, sourcefile)
925 925 else:
926 926 current_file = selectfile(afile, bfile, current_hunk,
927 927 strip, reverse)
928 928 current_file = patchfile(ui, current_file)
929 929 hunknum += 1
930 930 current_hunk.extract(fp)
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 fp, dopatch, gitpatches = scangitpatch(fp, x)
939 939 for gp in gitpatches:
940 940 changed[gp.path] = (gp.op, gp)
941 941 # else error?
942 942 # copy/rename + modify should modify target, not source
943 943 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
944 944 'RENAME'):
945 945 afile = bfile
946 946 gitworkdone = True
947 947 newfile = True
948 948 elif x.startswith('---'):
949 949 # check for a unified diff
950 950 l2 = lr.readline()
951 951 if not l2.startswith('+++'):
952 952 lr.push(l2)
953 953 continue
954 954 newfile = True
955 955 context = False
956 956 afile = parsefilename(x)
957 957 bfile = parsefilename(l2)
958 958 elif x.startswith('***'):
959 959 # check for a context diff
960 960 l2 = lr.readline()
961 961 if not l2.startswith('---'):
962 962 lr.push(l2)
963 963 continue
964 964 l3 = lr.readline()
965 965 lr.push(l3)
966 966 if not l3.startswith("***************"):
967 967 lr.push(l2)
968 968 continue
969 969 newfile = True
970 970 context = True
971 971 afile = parsefilename(x)
972 972 bfile = parsefilename(l2)
973 973
974 974 if newfile:
975 975 if current_file:
976 976 current_file.close()
977 977 if rejmerge:
978 978 rejmerge(current_file)
979 979 rejects += len(current_file.rej)
980 980 state = BFILE
981 981 current_file = None
982 982 hunknum = 0
983 983 if current_hunk:
984 984 if current_hunk.complete():
985 985 ret = current_file.apply(current_hunk, reverse)
986 986 if ret >= 0:
987 987 changed.setdefault(current_file.fname, (None, None))
988 988 if ret > 0:
989 989 err = 1
990 990 else:
991 991 fname = current_file and current_file.fname or None
992 992 raise PatchError(_("malformed patch %s %s") % (fname,
993 993 current_hunk.desc))
994 994 if current_file:
995 995 current_file.close()
996 996 if rejmerge:
997 997 rejmerge(current_file)
998 998 rejects += len(current_file.rej)
999 999 if updatedir and git:
1000 1000 updatedir(gitpatches)
1001 1001 if rejects:
1002 1002 return -1
1003 1003 if hunknum == 0 and dopatch and not gitworkdone:
1004 1004 raise NoHunks
1005 1005 return err
1006 1006
1007 1007 def diffopts(ui, opts={}, untrusted=False):
1008 1008 def get(key, name=None):
1009 1009 return (opts.get(key) or
1010 1010 ui.configbool('diff', name or key, None, untrusted=untrusted))
1011 1011 return mdiff.diffopts(
1012 1012 text=opts.get('text'),
1013 1013 git=get('git'),
1014 1014 nodates=get('nodates'),
1015 1015 showfunc=get('show_function', 'showfunc'),
1016 1016 ignorews=get('ignore_all_space', 'ignorews'),
1017 1017 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1018 1018 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
1019 1019
1020 1020 def updatedir(ui, repo, patches):
1021 1021 '''Update dirstate after patch application according to metadata'''
1022 1022 if not patches:
1023 1023 return
1024 1024 copies = []
1025 1025 removes = {}
1026 1026 cfiles = patches.keys()
1027 1027 cwd = repo.getcwd()
1028 1028 if cwd:
1029 1029 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1030 1030 for f in patches:
1031 1031 ctype, gp = patches[f]
1032 1032 if ctype == 'RENAME':
1033 1033 copies.append((gp.oldpath, gp.path, gp.copymod))
1034 1034 removes[gp.oldpath] = 1
1035 1035 elif ctype == 'COPY':
1036 1036 copies.append((gp.oldpath, gp.path, gp.copymod))
1037 1037 elif ctype == 'DELETE':
1038 1038 removes[gp.path] = 1
1039 1039 for src, dst, after in copies:
1040 1040 if not after:
1041 1041 copyfile(src, dst, repo.root)
1042 1042 repo.copy(src, dst)
1043 1043 removes = removes.keys()
1044 1044 if removes:
1045 1045 removes.sort()
1046 1046 repo.remove(removes, True)
1047 1047 for f in patches:
1048 1048 ctype, gp = patches[f]
1049 1049 if gp and gp.mode:
1050 1050 x = gp.mode & 0100 != 0
1051 1051 l = gp.mode & 020000 != 0
1052 1052 dst = os.path.join(repo.root, gp.path)
1053 1053 # patch won't create empty files
1054 1054 if ctype == 'ADD' and not os.path.exists(dst):
1055 1055 repo.wwrite(gp.path, '', x and 'x' or '')
1056 1056 else:
1057 1057 util.set_link(dst, l)
1058 1058 if not l:
1059 1059 util.set_exec(dst, x)
1060 1060 cmdutil.addremove(repo, cfiles)
1061 1061 files = patches.keys()
1062 1062 files.extend([r for r in removes if r not in files])
1063 1063 files.sort()
1064 1064
1065 1065 return files
1066 1066
1067 1067 def b85diff(to, tn):
1068 1068 '''print base85-encoded binary diff'''
1069 1069 def gitindex(text):
1070 1070 if not text:
1071 1071 return '0' * 40
1072 1072 l = len(text)
1073 1073 s = sha.new('blob %d\0' % l)
1074 1074 s.update(text)
1075 1075 return s.hexdigest()
1076 1076
1077 1077 def fmtline(line):
1078 1078 l = len(line)
1079 1079 if l <= 26:
1080 1080 l = chr(ord('A') + l - 1)
1081 1081 else:
1082 1082 l = chr(l - 26 + ord('a') - 1)
1083 1083 return '%c%s\n' % (l, base85.b85encode(line, True))
1084 1084
1085 1085 def chunk(text, csize=52):
1086 1086 l = len(text)
1087 1087 i = 0
1088 1088 while i < l:
1089 1089 yield text[i:i+csize]
1090 1090 i += csize
1091 1091
1092 1092 tohash = gitindex(to)
1093 1093 tnhash = gitindex(tn)
1094 1094 if tohash == tnhash:
1095 1095 return ""
1096 1096
1097 1097 # TODO: deltas
1098 1098 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1099 1099 (tohash, tnhash, len(tn))]
1100 1100 for l in chunk(zlib.compress(tn)):
1101 1101 ret.append(fmtline(l))
1102 1102 ret.append('\n')
1103 1103 return ''.join(ret)
1104 1104
1105 1105 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1106 1106 fp=None, changes=None, opts=None):
1107 1107 '''print diff of changes to files between two nodes, or node and
1108 1108 working directory.
1109 1109
1110 1110 if node1 is None, use first dirstate parent instead.
1111 1111 if node2 is None, compare node1 with working directory.'''
1112 1112
1113 1113 if opts is None:
1114 1114 opts = mdiff.defaultopts
1115 1115 if fp is None:
1116 1116 fp = repo.ui
1117 1117
1118 1118 if not node1:
1119 1119 node1 = repo.dirstate.parents()[0]
1120 1120
1121 1121 ccache = {}
1122 1122 def getctx(r):
1123 1123 if r not in ccache:
1124 1124 ccache[r] = context.changectx(repo, r)
1125 1125 return ccache[r]
1126 1126
1127 1127 flcache = {}
1128 1128 def getfilectx(f, ctx):
1129 1129 flctx = ctx.filectx(f, filelog=flcache.get(f))
1130 1130 if f not in flcache:
1131 1131 flcache[f] = flctx._filelog
1132 1132 return flctx
1133 1133
1134 1134 # reading the data for node1 early allows it to play nicely
1135 1135 # with repo.status and the revlog cache.
1136 1136 ctx1 = context.changectx(repo, node1)
1137 1137 # force manifest reading
1138 1138 man1 = ctx1.manifest()
1139 1139 date1 = util.datestr(ctx1.date())
1140 1140
1141 1141 if not changes:
1142 1142 changes = repo.status(node1, node2, files, match=match)[:5]
1143 1143 modified, added, removed, deleted, unknown = changes
1144 1144
1145 1145 if not modified and not added and not removed:
1146 1146 return
1147 1147
1148 1148 if node2:
1149 1149 ctx2 = context.changectx(repo, node2)
1150 1150 execf2 = ctx2.manifest().execf
1151 1151 linkf2 = ctx2.manifest().linkf
1152 1152 else:
1153 1153 ctx2 = context.workingctx(repo)
1154 1154 execf2 = util.execfunc(repo.root, None)
1155 1155 linkf2 = util.linkfunc(repo.root, None)
1156 1156 if execf2 is None:
1157 1157 mc = ctx2.parents()[0].manifest().copy()
1158 1158 execf2 = mc.execf
1159 1159 linkf2 = mc.linkf
1160 1160
1161 1161 # returns False if there was no rename between ctx1 and ctx2
1162 1162 # returns None if the file was created between ctx1 and ctx2
1163 1163 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1164 def renamed(f):
1165 startrev = ctx1.rev()
1166 c = ctx2
1164 # This will only really work if c1 is the Nth 1st parent of c2.
1165 def renamed(c1, c2, man, f):
1166 startrev = c1.rev()
1167 c = c2
1167 1168 crev = c.rev()
1168 1169 if crev is None:
1169 1170 crev = repo.changelog.count()
1170 1171 orig = f
1171 1172 while crev > startrev:
1172 1173 if f in c.files():
1173 1174 try:
1174 1175 src = getfilectx(f, c).renamed()
1175 1176 except revlog.LookupError:
1176 1177 return None
1177 1178 if src:
1178 1179 f = src[0]
1179 1180 crev = c.parents()[0].rev()
1180 1181 # try to reuse
1181 1182 c = getctx(crev)
1182 if f not in man1:
1183 if f not in man:
1183 1184 return None
1184 1185 if f == orig:
1185 1186 return False
1186 1187 return f
1187 1188
1188 1189 if repo.ui.quiet:
1189 1190 r = None
1190 1191 else:
1191 1192 hexfunc = repo.ui.debugflag and hex or short
1192 1193 r = [hexfunc(node) for node in [node1, node2] if node]
1193 1194
1194 1195 if opts.git:
1195 1196 copied = {}
1196 for f in added:
1197 src = renamed(f)
1197 c1, c2 = ctx1, ctx2
1198 files = added
1199 man = man1
1200 if node2 and ctx1.rev() >= ctx2.rev():
1201 # renamed() starts at c2 and walks back in history until c1.
1202 # Since ctx1.rev() >= ctx2.rev(), invert ctx2 and ctx1 to
1203 # detect (inverted) copies.
1204 c1, c2 = ctx2, ctx1
1205 files = removed
1206 man = ctx2.manifest()
1207 for f in files:
1208 src = renamed(c1, c2, man, f)
1198 1209 if src:
1199 1210 copied[f] = src
1200 srcs = [x[1] for x in copied.items()]
1211 if ctx1 == c2:
1212 # invert the copied dict
1213 copied = dict([(v, k) for (k, v) in copied.iteritems()])
1214 # If we've renamed file foo to bar (copied['bar'] = 'foo'),
1215 # avoid showing a diff for foo if we're going to show
1216 # the rename to bar.
1217 srcs = [x[1] for x in copied.iteritems() if x[0] in added]
1201 1218
1202 1219 all = modified + added + removed
1203 1220 all.sort()
1204 1221 gone = {}
1205 1222
1206 1223 for f in all:
1207 1224 to = None
1208 1225 tn = None
1209 1226 dodiff = True
1210 1227 header = []
1211 1228 if f in man1:
1212 1229 to = getfilectx(f, ctx1).data()
1213 1230 if f not in removed:
1214 1231 tn = getfilectx(f, ctx2).data()
1215 1232 if opts.git:
1216 1233 def gitmode(x, l):
1217 1234 return l and '120000' or (x and '100755' or '100644')
1218 1235 def addmodehdr(header, omode, nmode):
1219 1236 if omode != nmode:
1220 1237 header.append('old mode %s\n' % omode)
1221 1238 header.append('new mode %s\n' % nmode)
1222 1239
1223 1240 a, b = f, f
1224 1241 if f in added:
1225 1242 mode = gitmode(execf2(f), linkf2(f))
1226 1243 if f in copied:
1227 1244 a = copied[f]
1228 1245 omode = gitmode(man1.execf(a), man1.linkf(a))
1229 1246 addmodehdr(header, omode, mode)
1230 1247 if a in removed and a not in gone:
1231 1248 op = 'rename'
1232 1249 gone[a] = 1
1233 1250 else:
1234 1251 op = 'copy'
1235 1252 header.append('%s from %s\n' % (op, a))
1236 1253 header.append('%s to %s\n' % (op, f))
1237 1254 to = getfilectx(a, ctx1).data()
1238 1255 else:
1239 1256 header.append('new file mode %s\n' % mode)
1240 1257 if util.binary(tn):
1241 1258 dodiff = 'binary'
1242 1259 elif f in removed:
1243 1260 if f in srcs:
1244 1261 dodiff = False
1245 1262 else:
1246 1263 mode = gitmode(man1.execf(f), man1.linkf(f))
1247 1264 header.append('deleted file mode %s\n' % mode)
1248 1265 else:
1249 1266 omode = gitmode(man1.execf(f), man1.linkf(f))
1250 1267 nmode = gitmode(execf2(f), linkf2(f))
1251 1268 addmodehdr(header, omode, nmode)
1252 1269 if util.binary(to) or util.binary(tn):
1253 1270 dodiff = 'binary'
1254 1271 r = None
1255 1272 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1256 1273 if dodiff:
1257 1274 if dodiff == 'binary':
1258 1275 text = b85diff(to, tn)
1259 1276 else:
1260 1277 text = mdiff.unidiff(to, date1,
1261 1278 # ctx2 date may be dynamic
1262 1279 tn, util.datestr(ctx2.date()),
1263 1280 f, r, opts=opts)
1264 1281 if text or len(header) > 1:
1265 1282 fp.write(''.join(header))
1266 1283 fp.write(text)
1267 1284
1268 1285 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1269 1286 opts=None):
1270 1287 '''export changesets as hg patches.'''
1271 1288
1272 1289 total = len(revs)
1273 1290 revwidth = max([len(str(rev)) for rev in revs])
1274 1291
1275 1292 def single(rev, seqno, fp):
1276 1293 ctx = repo.changectx(rev)
1277 1294 node = ctx.node()
1278 1295 parents = [p.node() for p in ctx.parents() if p]
1279 1296 branch = ctx.branch()
1280 1297 if switch_parent:
1281 1298 parents.reverse()
1282 1299 prev = (parents and parents[0]) or nullid
1283 1300
1284 1301 if not fp:
1285 1302 fp = cmdutil.make_file(repo, template, node, total=total,
1286 1303 seqno=seqno, revwidth=revwidth)
1287 1304 if fp != sys.stdout and hasattr(fp, 'name'):
1288 1305 repo.ui.note("%s\n" % fp.name)
1289 1306
1290 1307 fp.write("# HG changeset patch\n")
1291 1308 fp.write("# User %s\n" % ctx.user())
1292 1309 fp.write("# Date %d %d\n" % ctx.date())
1293 1310 if branch and (branch != 'default'):
1294 1311 fp.write("# Branch %s\n" % branch)
1295 1312 fp.write("# Node ID %s\n" % hex(node))
1296 1313 fp.write("# Parent %s\n" % hex(prev))
1297 1314 if len(parents) > 1:
1298 1315 fp.write("# Parent %s\n" % hex(parents[1]))
1299 1316 fp.write(ctx.description().rstrip())
1300 1317 fp.write("\n\n")
1301 1318
1302 1319 diff(repo, prev, node, fp=fp, opts=opts)
1303 1320 if fp not in (sys.stdout, repo.ui):
1304 1321 fp.close()
1305 1322
1306 1323 for seqno, rev in enumerate(revs):
1307 1324 single(rev, seqno+1, fp)
1308 1325
1309 1326 def diffstat(patchlines):
1310 1327 if not util.find_exe('diffstat'):
1311 1328 return
1312 1329 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1313 1330 try:
1314 1331 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1315 1332 try:
1316 1333 for line in patchlines: print >> p.tochild, line
1317 1334 p.tochild.close()
1318 1335 if p.wait(): return
1319 1336 fp = os.fdopen(fd, 'r')
1320 1337 stat = []
1321 1338 for line in fp: stat.append(line.lstrip())
1322 1339 last = stat.pop()
1323 1340 stat.insert(0, last)
1324 1341 stat = ''.join(stat)
1325 1342 if stat.startswith('0 files'): raise ValueError
1326 1343 return stat
1327 1344 except: raise
1328 1345 finally:
1329 1346 try: os.unlink(name)
1330 1347 except: pass
@@ -1,148 +1,158 b''
1 1 #!/bin/sh
2 2
3 3 hg init a
4 4 cd a
5 5
6 6 echo start > start
7 7 hg ci -Amstart -d '0 0'
8 8 echo new > new
9 9 hg ci -Amnew -d '0 0'
10 10 echo '% new file'
11 11 hg diff --git -r 0
12 12
13 13 hg cp new copy
14 14 hg ci -mcopy -d '0 0'
15 15 echo '% copy'
16 16 hg diff --git -r 1:tip
17 17
18 18 hg mv copy rename
19 19 hg ci -mrename -d '0 0'
20 20 echo '% rename'
21 21 hg diff --git -r 2:tip
22 22
23 23 hg rm rename
24 24 hg ci -mdelete -d '0 0'
25 25 echo '% delete'
26 26 hg diff --git -r 3:tip
27 27
28 28 cat > src <<EOF
29 29 1
30 30 2
31 31 3
32 32 4
33 33 5
34 34 EOF
35 35 hg ci -Amsrc -d '0 0'
36 36 chmod +x src
37 37 hg ci -munexec -d '0 0'
38 38 echo '% chmod 644'
39 39 hg diff --git -r 5:tip
40 40
41 41 hg mv src dst
42 42 chmod -x dst
43 43 echo a >> dst
44 44 hg ci -mrenamemod -d '0 0'
45 45 echo '% rename+mod+chmod'
46 46 hg diff --git -r 6:tip
47 47
48 48 echo '% nonexistent in tip+chmod'
49 49 hg diff --git -r 5:6
50 50
51 51 echo '% binary diff'
52 52 cp $TESTDIR/binfile.bin .
53 53 hg add binfile.bin
54 54 hg diff --git > b.diff
55 55 cat b.diff
56 56
57 57 echo '% import binary diff'
58 58 hg revert binfile.bin
59 59 rm binfile.bin
60 60 hg import -mfoo b.diff
61 61 cmp binfile.bin $TESTDIR/binfile.bin
62 62
63 63 echo
64 64 echo '% rename binary file'
65 65 hg mv binfile.bin renamed.bin
66 66 hg diff --git
67 67
68 68 echo
69 69 echo '% diff across many revisions'
70 70 hg mv dst dst2
71 71 hg ci -m 'mv dst dst2' -d '0 0'
72 72
73 73 echo >> start
74 74 hg ci -m 'change start' -d '0 0'
75 75
76 76 hg revert -r -2 start
77 77 hg mv dst2 dst3
78 78 hg ci -m 'mv dst2 dst3; revert start' -d '0 0'
79 79
80 80 hg diff --git -r 9:11
81 echo '% reversed'
82 hg diff --git -r 11:9
81 83
82 84 echo a >> foo
83 85 hg add foo
84 86 hg ci -m 'add foo'
85 87 echo b >> foo
86 88 hg ci -m 'change foo'
87 89 hg mv foo bar
88 90 hg ci -m 'mv foo bar'
89 91 echo c >> bar
90 92 hg ci -m 'change bar'
91 93
92 94 echo
93 95 echo '% file created before r1 and renamed before r2'
94 96 hg diff --git -r -3:-1
97 echo '% reversed'
98 hg diff --git -r -1:-3
95 99 echo
96 100 echo '% file created in r1 and renamed before r2'
97 101 hg diff --git -r -4:-1
102 echo '% reversed'
103 hg diff --git -r -1:-4
98 104 echo
99 105 echo '% file created after r1 and renamed before r2'
100 106 hg diff --git -r -5:-1
107 echo '% reversed'
108 hg diff --git -r -1:-5
101 109
102 110 echo
103 111 echo '% comparing with the working dir'
104 112 echo >> start
105 113 hg ci -m 'change start again' -d '0 0'
106 114
107 115 echo > created
108 116 hg add created
109 117 hg ci -m 'add created'
110 118
111 119 hg mv created created2
112 120 hg ci -m 'mv created created2'
113 121
114 122 hg mv created2 created3
115 123 echo "% there's a copy in the working dir..."
116 124 hg diff --git
117 125 echo
118 126 echo "% ...but there's another copy between the original rev and the wd"
119 127 hg diff --git -r -2
120 128 echo
121 129 echo "% ...but the source of the copy was created after the original rev"
122 130 hg diff --git -r -3
123 131 hg ci -m 'mv created2 created3'
124 132
125 133 echo > brand-new
126 134 hg add brand-new
127 135 hg ci -m 'add brand-new'
128 136 hg mv brand-new brand-new2
129 137 echo '% created in parent of wd; renamed in the wd'
130 138 hg diff --git
131 139
132 140 echo
133 141 echo '% created between r1 and parent of wd; renamed in the wd'
134 142 hg diff --git -r -2
135 143 hg ci -m 'mv brand-new brand-new2'
136 144
137 145 echo '% one file is copied to many destinations and removed'
138 146 hg cp brand-new2 brand-new3
139 147 hg mv brand-new2 brand-new3-2
140 148 hg ci -m 'multiple renames/copies'
141 149 hg diff --git -r -2 -r -1
150 echo '% reversed'
151 hg diff --git -r -1 -r -2
142 152
143 153 echo '% there should be a trailing TAB if there are spaces in the file name'
144 154 echo foo > 'with spaces'
145 155 hg add 'with spaces'
146 156 hg diff --git
147 157 hg ci -m 'add filename with spaces'
148 158
@@ -1,154 +1,197 b''
1 1 adding start
2 2 adding new
3 3 % new file
4 4 diff --git a/new b/new
5 5 new file mode 100644
6 6 --- /dev/null
7 7 +++ b/new
8 8 @@ -0,0 +1,1 @@
9 9 +new
10 10 % copy
11 11 diff --git a/new b/copy
12 12 copy from new
13 13 copy to copy
14 14 % rename
15 15 diff --git a/copy b/rename
16 16 rename from copy
17 17 rename to rename
18 18 % delete
19 19 diff --git a/rename b/rename
20 20 deleted file mode 100644
21 21 --- a/rename
22 22 +++ /dev/null
23 23 @@ -1,1 +0,0 @@
24 24 -new
25 25 adding src
26 26 % chmod 644
27 27 diff --git a/src b/src
28 28 old mode 100644
29 29 new mode 100755
30 30 % rename+mod+chmod
31 31 diff --git a/src b/dst
32 32 old mode 100755
33 33 new mode 100644
34 34 rename from src
35 35 rename to dst
36 36 --- a/dst
37 37 +++ b/dst
38 38 @@ -3,3 +3,4 @@ 3
39 39 3
40 40 4
41 41 5
42 42 +a
43 43 % nonexistent in tip+chmod
44 44 diff --git a/src b/src
45 45 old mode 100644
46 46 new mode 100755
47 47 % binary diff
48 48 diff --git a/binfile.bin b/binfile.bin
49 49 new file mode 100644
50 50 index 0000000000000000000000000000000000000000..37ba3d1c6f17137d9c5f5776fa040caf5fe73ff9
51 51 GIT binary patch
52 52 literal 593
53 53 zc$@)I0<QguP)<h;3K|Lk000e1NJLTq000mG000mO0ssI2kdbIM00009a7bBm000XU
54 54 z000XU0RWnu7ytkO2XskIMF-Uh9TW;VpMjwv0005-Nkl<ZD9@FWPs=e;7{<>W$NUkd
55 55 zX$nnYLt$-$V!?uy+1V%`z&Eh=ah|duER<4|QWhju3gb^nF*8iYobxWG-qqXl=2~5M
56 56 z*IoDB)sG^CfNuoBmqLTVU^<;@nwHP!1wrWd`{(mHo6VNXWtyh{alzqmsH*yYzpvLT
57 57 zLdY<T=ks|woh-`&01!ej#(xbV1f|pI*=%;d-%F*E*X#ZH`4I%6SS+$EJDE&ct=8po
58 58 ziN#{?_j|kD%Cd|oiqds`xm@;oJ-^?NG3Gdqrs?5u*zI;{nogxsx~^|Fn^Y?Gdc6<;
59 59 zfMJ+iF1J`LMx&A2?dEwNW8ClebzPTbIh{@$hS6*`kH@1d%Lo7fA#}N1)oN7`gm$~V
60 60 z+wDx#)OFqMcE{s!JN0-xhG8ItAjVkJwEcb`3WWlJfU2r?;Pd%dmR+q@mSri5q9_W-
61 61 zaR2~ECX?B2w+zELozC0s*6Z~|QG^f{3I#<`?)Q7U-JZ|q5W;9Q8i_=pBuSzunx=U;
62 62 z9C)5jBoYw9^?EHyQl(M}1OlQcCX>lXB*ODN003Z&P17_@)3Pi=i0wb04<W?v-u}7K
63 63 zXmmQA+wDgE!qR9o8jr`%=ab_&uh(l?R=r;Tjiqon91I2-hIu?57~@*4h7h9uORK#=
64 64 fQItJW-{SoTm)8|5##k|m00000NkvXXu0mjf{mKw4
65 65
66 66 % import binary diff
67 67 applying b.diff
68 68
69 69 % rename binary file
70 70 diff --git a/binfile.bin b/renamed.bin
71 71 rename from binfile.bin
72 72 rename to renamed.bin
73 73
74 74 % diff across many revisions
75 75 diff --git a/dst2 b/dst3
76 76 rename from dst2
77 77 rename to dst3
78 % reversed
79 diff --git a/dst3 b/dst2
80 rename from dst3
81 rename to dst2
78 82
79 83 % file created before r1 and renamed before r2
80 84 diff --git a/foo b/bar
81 85 rename from foo
82 86 rename to bar
83 87 --- a/bar
84 88 +++ b/bar
85 89 @@ -1,2 +1,3 @@ a
86 90 a
87 91 b
88 92 +c
93 % reversed
94 diff --git a/bar b/foo
95 rename from bar
96 rename to foo
97 --- a/foo
98 +++ b/foo
99 @@ -1,3 +1,2 @@ a
100 a
101 b
102 -c
89 103
90 104 % file created in r1 and renamed before r2
91 105 diff --git a/foo b/bar
92 106 rename from foo
93 107 rename to bar
94 108 --- a/bar
95 109 +++ b/bar
96 110 @@ -1,1 +1,3 @@ a
97 111 a
98 112 +b
99 113 +c
114 % reversed
115 diff --git a/bar b/foo
116 rename from bar
117 rename to foo
118 --- a/foo
119 +++ b/foo
120 @@ -1,3 +1,1 @@ a
121 a
122 -b
123 -c
100 124
101 125 % file created after r1 and renamed before r2
102 126 diff --git a/bar b/bar
103 127 new file mode 100644
104 128 --- /dev/null
105 129 +++ b/bar
106 130 @@ -0,0 +1,3 @@
107 131 +a
108 132 +b
109 133 +c
134 % reversed
135 diff --git a/bar b/bar
136 deleted file mode 100644
137 --- a/bar
138 +++ /dev/null
139 @@ -1,3 +0,0 @@
140 -a
141 -b
142 -c
110 143
111 144 % comparing with the working dir
112 145 % there's a copy in the working dir...
113 146 diff --git a/created2 b/created3
114 147 rename from created2
115 148 rename to created3
116 149
117 150 % ...but there's another copy between the original rev and the wd
118 151 diff --git a/created b/created3
119 152 rename from created
120 153 rename to created3
121 154
122 155 % ...but the source of the copy was created after the original rev
123 156 diff --git a/created3 b/created3
124 157 new file mode 100644
125 158 --- /dev/null
126 159 +++ b/created3
127 160 @@ -0,0 +1,1 @@
128 161 +
129 162 % created in parent of wd; renamed in the wd
130 163 diff --git a/brand-new b/brand-new2
131 164 rename from brand-new
132 165 rename to brand-new2
133 166
134 167 % created between r1 and parent of wd; renamed in the wd
135 168 diff --git a/brand-new2 b/brand-new2
136 169 new file mode 100644
137 170 --- /dev/null
138 171 +++ b/brand-new2
139 172 @@ -0,0 +1,1 @@
140 173 +
141 174 % one file is copied to many destinations and removed
142 175 diff --git a/brand-new2 b/brand-new3
143 176 rename from brand-new2
144 177 rename to brand-new3
145 178 diff --git a/brand-new2 b/brand-new3-2
146 179 copy from brand-new2
147 180 copy to brand-new3-2
181 % reversed
182 diff --git a/brand-new3 b/brand-new2
183 rename from brand-new3
184 rename to brand-new2
185 diff --git a/brand-new3-2 b/brand-new3-2
186 deleted file mode 100644
187 --- a/brand-new3-2
188 +++ /dev/null
189 @@ -1,1 +0,0 @@
190 -
148 191 % there should be a trailing TAB if there are spaces in the file name
149 192 diff --git a/with spaces b/with spaces
150 193 new file mode 100644
151 194 --- /dev/null
152 195 +++ b/with spaces
153 196 @@ -0,0 +1,1 @@
154 197 +foo
General Comments 0
You need to be logged in to leave comments. Login now