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