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