##// END OF EJS Templates
Check that git patches only touch files under root
Brendan Cully -
r6791:bbd89c9e default
parent child Browse files
Show More
@@ -1,1351 +1,1354 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 hex, nullid, short
11 11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers, copies
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 508 def __init__(self, desc, num, lr, context, create=False, remove=False):
509 509 self.number = num
510 510 self.desc = desc
511 511 self.hunk = [ desc ]
512 512 self.a = []
513 513 self.b = []
514 514 if context:
515 515 self.read_context_hunk(lr)
516 516 else:
517 517 self.read_unified_hunk(lr)
518 518 self.create = create
519 519 self.remove = remove and not create
520 520
521 521 def read_unified_hunk(self, lr):
522 522 m = unidesc.match(self.desc)
523 523 if not m:
524 524 raise PatchError(_("bad hunk #%d") % self.number)
525 525 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
526 526 if self.lena == None:
527 527 self.lena = 1
528 528 else:
529 529 self.lena = int(self.lena)
530 530 if self.lenb == None:
531 531 self.lenb = 1
532 532 else:
533 533 self.lenb = int(self.lenb)
534 534 self.starta = int(self.starta)
535 535 self.startb = int(self.startb)
536 536 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
537 537 # if we hit eof before finishing out the hunk, the last line will
538 538 # be zero length. Lets try to fix it up.
539 539 while len(self.hunk[-1]) == 0:
540 540 del self.hunk[-1]
541 541 del self.a[-1]
542 542 del self.b[-1]
543 543 self.lena -= 1
544 544 self.lenb -= 1
545 545
546 546 def read_context_hunk(self, lr):
547 547 self.desc = lr.readline()
548 548 m = contextdesc.match(self.desc)
549 549 if not m:
550 550 raise PatchError(_("bad hunk #%d") % self.number)
551 551 foo, self.starta, foo2, aend, foo3 = m.groups()
552 552 self.starta = int(self.starta)
553 553 if aend == None:
554 554 aend = self.starta
555 555 self.lena = int(aend) - self.starta
556 556 if self.starta:
557 557 self.lena += 1
558 558 for x in xrange(self.lena):
559 559 l = lr.readline()
560 560 if l.startswith('---'):
561 561 lr.push(l)
562 562 break
563 563 s = l[2:]
564 564 if l.startswith('- ') or l.startswith('! '):
565 565 u = '-' + s
566 566 elif l.startswith(' '):
567 567 u = ' ' + s
568 568 else:
569 569 raise PatchError(_("bad hunk #%d old text line %d") %
570 570 (self.number, x))
571 571 self.a.append(u)
572 572 self.hunk.append(u)
573 573
574 574 l = lr.readline()
575 575 if l.startswith('\ '):
576 576 s = self.a[-1][:-1]
577 577 self.a[-1] = s
578 578 self.hunk[-1] = s
579 579 l = lr.readline()
580 580 m = contextdesc.match(l)
581 581 if not m:
582 582 raise PatchError(_("bad hunk #%d") % self.number)
583 583 foo, self.startb, foo2, bend, foo3 = m.groups()
584 584 self.startb = int(self.startb)
585 585 if bend == None:
586 586 bend = self.startb
587 587 self.lenb = int(bend) - self.startb
588 588 if self.startb:
589 589 self.lenb += 1
590 590 hunki = 1
591 591 for x in xrange(self.lenb):
592 592 l = lr.readline()
593 593 if l.startswith('\ '):
594 594 s = self.b[-1][:-1]
595 595 self.b[-1] = s
596 596 self.hunk[hunki-1] = s
597 597 continue
598 598 if not l:
599 599 lr.push(l)
600 600 break
601 601 s = l[2:]
602 602 if l.startswith('+ ') or l.startswith('! '):
603 603 u = '+' + s
604 604 elif l.startswith(' '):
605 605 u = ' ' + s
606 606 elif len(self.b) == 0:
607 607 # this can happen when the hunk does not add any lines
608 608 lr.push(l)
609 609 break
610 610 else:
611 611 raise PatchError(_("bad hunk #%d old text line %d") %
612 612 (self.number, x))
613 613 self.b.append(s)
614 614 while True:
615 615 if hunki >= len(self.hunk):
616 616 h = ""
617 617 else:
618 618 h = self.hunk[hunki]
619 619 hunki += 1
620 620 if h == u:
621 621 break
622 622 elif h.startswith('-'):
623 623 continue
624 624 else:
625 625 self.hunk.insert(hunki-1, u)
626 626 break
627 627
628 628 if not self.a:
629 629 # this happens when lines were only added to the hunk
630 630 for x in self.hunk:
631 631 if x.startswith('-') or x.startswith(' '):
632 632 self.a.append(x)
633 633 if not self.b:
634 634 # this happens when lines were only deleted from the hunk
635 635 for x in self.hunk:
636 636 if x.startswith('+') or x.startswith(' '):
637 637 self.b.append(x[1:])
638 638 # @@ -start,len +start,len @@
639 639 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
640 640 self.startb, self.lenb)
641 641 self.hunk[0] = self.desc
642 642
643 643 def reverse(self):
644 644 self.create, self.remove = self.remove, self.create
645 645 origlena = self.lena
646 646 origstarta = self.starta
647 647 self.lena = self.lenb
648 648 self.starta = self.startb
649 649 self.lenb = origlena
650 650 self.startb = origstarta
651 651 self.a = []
652 652 self.b = []
653 653 # self.hunk[0] is the @@ description
654 654 for x in xrange(1, len(self.hunk)):
655 655 o = self.hunk[x]
656 656 if o.startswith('-'):
657 657 n = '+' + o[1:]
658 658 self.b.append(o[1:])
659 659 elif o.startswith('+'):
660 660 n = '-' + o[1:]
661 661 self.a.append(n)
662 662 else:
663 663 n = o
664 664 self.b.append(o[1:])
665 665 self.a.append(o)
666 666 self.hunk[x] = o
667 667
668 668 def fix_newline(self):
669 669 diffhelpers.fix_newline(self.hunk, self.a, self.b)
670 670
671 671 def complete(self):
672 672 return len(self.a) == self.lena and len(self.b) == self.lenb
673 673
674 674 def createfile(self):
675 675 return self.starta == 0 and self.lena == 0 and self.create
676 676
677 677 def rmfile(self):
678 678 return self.startb == 0 and self.lenb == 0 and self.remove
679 679
680 680 def fuzzit(self, l, fuzz, toponly):
681 681 # this removes context lines from the top and bottom of list 'l'. It
682 682 # checks the hunk to make sure only context lines are removed, and then
683 683 # returns a new shortened list of lines.
684 684 fuzz = min(fuzz, len(l)-1)
685 685 if fuzz:
686 686 top = 0
687 687 bot = 0
688 688 hlen = len(self.hunk)
689 689 for x in xrange(hlen-1):
690 690 # the hunk starts with the @@ line, so use x+1
691 691 if self.hunk[x+1][0] == ' ':
692 692 top += 1
693 693 else:
694 694 break
695 695 if not toponly:
696 696 for x in xrange(hlen-1):
697 697 if self.hunk[hlen-bot-1][0] == ' ':
698 698 bot += 1
699 699 else:
700 700 break
701 701
702 702 # top and bot now count context in the hunk
703 703 # adjust them if either one is short
704 704 context = max(top, bot, 3)
705 705 if bot < context:
706 706 bot = max(0, fuzz - (context - bot))
707 707 else:
708 708 bot = min(fuzz, bot)
709 709 if top < context:
710 710 top = max(0, fuzz - (context - top))
711 711 else:
712 712 top = min(fuzz, top)
713 713
714 714 return l[top:len(l)-bot]
715 715 return l
716 716
717 717 def old(self, fuzz=0, toponly=False):
718 718 return self.fuzzit(self.a, fuzz, toponly)
719 719
720 720 def newctrl(self):
721 721 res = []
722 722 for x in self.hunk:
723 723 c = x[0]
724 724 if c == ' ' or c == '+':
725 725 res.append(x)
726 726 return res
727 727
728 728 def new(self, fuzz=0, toponly=False):
729 729 return self.fuzzit(self.b, fuzz, toponly)
730 730
731 731 class binhunk:
732 732 'A binary patch file. Only understands literals so far.'
733 733 def __init__(self, gitpatch):
734 734 self.gitpatch = gitpatch
735 735 self.text = None
736 736 self.hunk = ['GIT binary patch\n']
737 737
738 738 def createfile(self):
739 739 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
740 740
741 741 def rmfile(self):
742 742 return self.gitpatch.op == 'DELETE'
743 743
744 744 def complete(self):
745 745 return self.text is not None
746 746
747 747 def new(self):
748 748 return [self.text]
749 749
750 750 def extract(self, fp):
751 751 line = fp.readline()
752 752 self.hunk.append(line)
753 753 while line and not line.startswith('literal '):
754 754 line = fp.readline()
755 755 self.hunk.append(line)
756 756 if not line:
757 757 raise PatchError(_('could not extract binary patch'))
758 758 size = int(line[8:].rstrip())
759 759 dec = []
760 760 line = fp.readline()
761 761 self.hunk.append(line)
762 762 while len(line) > 1:
763 763 l = line[0]
764 764 if l <= 'Z' and l >= 'A':
765 765 l = ord(l) - ord('A') + 1
766 766 else:
767 767 l = ord(l) - ord('a') + 27
768 768 dec.append(base85.b85decode(line[1:-1])[:l])
769 769 line = fp.readline()
770 770 self.hunk.append(line)
771 771 text = zlib.decompress(''.join(dec))
772 772 if len(text) != size:
773 773 raise PatchError(_('binary patch is %d bytes, not %d') %
774 774 len(text), size)
775 775 self.text = text
776 776
777 777 def parsefilename(str):
778 778 # --- filename \t|space stuff
779 779 s = str[4:].rstrip('\r\n')
780 780 i = s.find('\t')
781 781 if i < 0:
782 782 i = s.find(' ')
783 783 if i < 0:
784 784 return s
785 785 return s[:i]
786 786
787 787 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
788 788 def pathstrip(path, count=1):
789 789 pathlen = len(path)
790 790 i = 0
791 791 if count == 0:
792 792 return '', path.rstrip()
793 793 while count > 0:
794 794 i = path.find('/', i)
795 795 if i == -1:
796 796 raise PatchError(_("unable to strip away %d dirs from %s") %
797 797 (count, path))
798 798 i += 1
799 799 # consume '//' in the path
800 800 while i < pathlen - 1 and path[i] == '/':
801 801 i += 1
802 802 count -= 1
803 803 return path[:i].lstrip(), path[i:].rstrip()
804 804
805 805 nulla = afile_orig == "/dev/null"
806 806 nullb = bfile_orig == "/dev/null"
807 807 abase, afile = pathstrip(afile_orig, strip)
808 808 gooda = not nulla and os.path.exists(afile)
809 809 bbase, bfile = pathstrip(bfile_orig, strip)
810 810 if afile == bfile:
811 811 goodb = gooda
812 812 else:
813 813 goodb = not nullb and os.path.exists(bfile)
814 814 createfunc = hunk.createfile
815 815 if reverse:
816 816 createfunc = hunk.rmfile
817 817 missing = not goodb and not gooda and not createfunc()
818 818 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
819 819 # diff is between a file and its backup. In this case, the original
820 820 # file should be patched (see original mpatch code).
821 821 isbackup = (abase == bbase and bfile.startswith(afile))
822 822 fname = None
823 823 if not missing:
824 824 if gooda and goodb:
825 825 fname = isbackup and afile or bfile
826 826 elif gooda:
827 827 fname = afile
828 828
829 829 if not fname:
830 830 if not nullb:
831 831 fname = isbackup and afile or bfile
832 832 elif not nulla:
833 833 fname = afile
834 834 else:
835 835 raise PatchError(_("undefined source and destination files"))
836 836
837 837 return fname, missing
838 838
839 839 class linereader:
840 840 # simple class to allow pushing lines back into the input stream
841 841 def __init__(self, fp):
842 842 self.fp = fp
843 843 self.buf = []
844 844
845 845 def push(self, line):
846 846 self.buf.append(line)
847 847
848 848 def readline(self):
849 849 if self.buf:
850 850 l = self.buf[0]
851 851 del self.buf[0]
852 852 return l
853 853 return self.fp.readline()
854 854
855 855 def iterhunks(ui, fp, sourcefile=None):
856 856 """Read a patch and yield the following events:
857 857 - ("file", afile, bfile, firsthunk): select a new target file.
858 858 - ("hunk", hunk): a new hunk is ready to be applied, follows a
859 859 "file" event.
860 860 - ("git", gitchanges): current diff is in git format, gitchanges
861 861 maps filenames to gitpatch records. Unique event.
862 862 """
863 863
864 864 def scangitpatch(fp, firstline):
865 865 '''git patches can modify a file, then copy that file to
866 866 a new file, but expect the source to be the unmodified form.
867 867 So we scan the patch looking for that case so we can do
868 868 the copies ahead of time.'''
869 869
870 870 pos = 0
871 871 try:
872 872 pos = fp.tell()
873 873 except IOError:
874 874 fp = cStringIO.StringIO(fp.read())
875 875
876 876 (dopatch, gitpatches) = readgitpatch(fp, firstline)
877 877 fp.seek(pos)
878 878
879 879 return fp, dopatch, gitpatches
880 880
881 881 changed = {}
882 882 current_hunk = None
883 883 afile = ""
884 884 bfile = ""
885 885 state = None
886 886 hunknum = 0
887 887 emitfile = False
888 888
889 889 git = False
890 890 gitre = re.compile('diff --git (a/.*) (b/.*)')
891 891
892 892 # our states
893 893 BFILE = 1
894 894 context = None
895 895 lr = linereader(fp)
896 896 dopatch = True
897 897 # gitworkdone is True if a git operation (copy, rename, ...) was
898 898 # performed already for the current file. Useful when the file
899 899 # section may have no hunk.
900 900 gitworkdone = False
901 901
902 902 while True:
903 903 newfile = False
904 904 x = lr.readline()
905 905 if not x:
906 906 break
907 907 if current_hunk:
908 908 if x.startswith('\ '):
909 909 current_hunk.fix_newline()
910 910 yield 'hunk', current_hunk
911 911 current_hunk = None
912 912 gitworkdone = False
913 913 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
914 914 ((context or context == None) and x.startswith('***************')))):
915 915 try:
916 916 if context == None and x.startswith('***************'):
917 917 context = True
918 918 gpatch = changed.get(bfile[2:], (None, None))[1]
919 919 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
920 920 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
921 921 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
922 922 except PatchError, err:
923 923 ui.debug(err)
924 924 current_hunk = None
925 925 continue
926 926 hunknum += 1
927 927 if emitfile:
928 928 emitfile = False
929 929 yield 'file', (afile, bfile, current_hunk)
930 930 elif state == BFILE and x.startswith('GIT binary patch'):
931 931 current_hunk = binhunk(changed[bfile[2:]][1])
932 932 hunknum += 1
933 933 if emitfile:
934 934 emitfile = False
935 935 yield 'file', (afile, bfile, current_hunk)
936 936 current_hunk.extract(fp)
937 937 elif x.startswith('diff --git'):
938 938 # check for git diff, scanning the whole patch file if needed
939 939 m = gitre.match(x)
940 940 if m:
941 941 afile, bfile = m.group(1, 2)
942 942 if not git:
943 943 git = True
944 944 fp, dopatch, gitpatches = scangitpatch(fp, x)
945 945 yield 'git', gitpatches
946 946 for gp in gitpatches:
947 947 changed[gp.path] = (gp.op, gp)
948 948 # else error?
949 949 # copy/rename + modify should modify target, not source
950 950 gitop = changed.get(bfile[2:], (None, None))[0]
951 951 if gitop in ('COPY', 'DELETE', 'RENAME'):
952 952 afile = bfile
953 953 gitworkdone = True
954 954 newfile = True
955 955 elif x.startswith('---'):
956 956 # check for a unified diff
957 957 l2 = lr.readline()
958 958 if not l2.startswith('+++'):
959 959 lr.push(l2)
960 960 continue
961 961 newfile = True
962 962 context = False
963 963 afile = parsefilename(x)
964 964 bfile = parsefilename(l2)
965 965 elif x.startswith('***'):
966 966 # check for a context diff
967 967 l2 = lr.readline()
968 968 if not l2.startswith('---'):
969 969 lr.push(l2)
970 970 continue
971 971 l3 = lr.readline()
972 972 lr.push(l3)
973 973 if not l3.startswith("***************"):
974 974 lr.push(l2)
975 975 continue
976 976 newfile = True
977 977 context = True
978 978 afile = parsefilename(x)
979 979 bfile = parsefilename(l2)
980 980
981 981 if newfile:
982 982 emitfile = True
983 983 state = BFILE
984 984 hunknum = 0
985 985 if current_hunk:
986 986 if current_hunk.complete():
987 987 yield 'hunk', current_hunk
988 988 else:
989 989 raise PatchError(_("malformed patch %s %s") % (afile,
990 990 current_hunk.desc))
991 991
992 992 if hunknum == 0 and dopatch and not gitworkdone:
993 993 raise NoHunks
994 994
995 995 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
996 996 rejmerge=None, updatedir=None):
997 997 """reads a patch from fp and tries to apply it. The dict 'changed' is
998 998 filled in with all of the filenames changed by the patch. Returns 0
999 999 for a clean patch, -1 if any rejects were found and 1 if there was
1000 1000 any fuzz."""
1001 1001
1002 1002 rejects = 0
1003 1003 err = 0
1004 1004 current_file = None
1005 1005 gitpatches = None
1006 1006
1007 1007 def closefile():
1008 1008 if not current_file:
1009 1009 return 0
1010 1010 current_file.close()
1011 1011 if rejmerge:
1012 1012 rejmerge(current_file)
1013 1013 return len(current_file.rej)
1014 1014
1015 1015 for state, values in iterhunks(ui, fp, sourcefile):
1016 1016 if state == 'hunk':
1017 1017 if not current_file:
1018 1018 continue
1019 1019 current_hunk = values
1020 1020 ret = current_file.apply(current_hunk, reverse)
1021 1021 if ret >= 0:
1022 1022 changed.setdefault(current_file.fname, (None, None))
1023 1023 if ret > 0:
1024 1024 err = 1
1025 1025 elif state == 'file':
1026 1026 rejects += closefile()
1027 1027 afile, bfile, first_hunk = values
1028 1028 try:
1029 1029 if sourcefile:
1030 1030 current_file = patchfile(ui, sourcefile)
1031 1031 else:
1032 1032 current_file, missing = selectfile(afile, bfile, first_hunk,
1033 1033 strip, reverse)
1034 1034 current_file = patchfile(ui, current_file, missing)
1035 1035 except PatchError, err:
1036 1036 ui.warn(str(err) + '\n')
1037 1037 current_file, current_hunk = None, None
1038 1038 rejects += 1
1039 1039 continue
1040 1040 elif state == 'git':
1041 1041 gitpatches = values
1042 cwd = os.getcwd()
1042 1043 for gp in gitpatches:
1043 1044 if gp.op in ('COPY', 'RENAME'):
1044 copyfile(gp.oldpath, gp.path)
1045 src, dst = [util.canonpath(cwd, cwd, x)
1046 for x in [gp.oldpath, gp.path]]
1047 copyfile(src, dst)
1045 1048 changed[gp.path] = (gp.op, gp)
1046 1049 else:
1047 1050 raise util.Abort(_('unsupported parser state: %s') % state)
1048 1051
1049 1052 rejects += closefile()
1050 1053
1051 1054 if updatedir and gitpatches:
1052 1055 updatedir(gitpatches)
1053 1056 if rejects:
1054 1057 return -1
1055 1058 return err
1056 1059
1057 1060 def diffopts(ui, opts={}, untrusted=False):
1058 1061 def get(key, name=None, getter=ui.configbool):
1059 1062 return (opts.get(key) or
1060 1063 getter('diff', name or key, None, untrusted=untrusted))
1061 1064 return mdiff.diffopts(
1062 1065 text=opts.get('text'),
1063 1066 git=get('git'),
1064 1067 nodates=get('nodates'),
1065 1068 showfunc=get('show_function', 'showfunc'),
1066 1069 ignorews=get('ignore_all_space', 'ignorews'),
1067 1070 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1068 1071 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1069 1072 context=get('unified', getter=ui.config))
1070 1073
1071 1074 def updatedir(ui, repo, patches):
1072 1075 '''Update dirstate after patch application according to metadata'''
1073 1076 if not patches:
1074 1077 return
1075 1078 copies = []
1076 1079 removes = {}
1077 1080 cfiles = patches.keys()
1078 1081 cwd = repo.getcwd()
1079 1082 if cwd:
1080 1083 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1081 1084 for f in patches:
1082 1085 ctype, gp = patches[f]
1083 1086 if ctype == 'RENAME':
1084 1087 copies.append((gp.oldpath, gp.path))
1085 1088 removes[gp.oldpath] = 1
1086 1089 elif ctype == 'COPY':
1087 1090 copies.append((gp.oldpath, gp.path))
1088 1091 elif ctype == 'DELETE':
1089 1092 removes[gp.path] = 1
1090 1093 for src, dst in copies:
1091 1094 repo.copy(src, dst)
1092 1095 removes = removes.keys()
1093 1096 if removes:
1094 1097 removes.sort()
1095 1098 repo.remove(removes, True)
1096 1099 for f in patches:
1097 1100 ctype, gp = patches[f]
1098 1101 if gp and gp.mode:
1099 1102 flags = ''
1100 1103 if gp.mode & 0100:
1101 1104 flags = 'x'
1102 1105 elif gp.mode & 020000:
1103 1106 flags = 'l'
1104 1107 dst = os.path.join(repo.root, gp.path)
1105 1108 # patch won't create empty files
1106 1109 if ctype == 'ADD' and not os.path.exists(dst):
1107 1110 repo.wwrite(gp.path, '', flags)
1108 1111 else:
1109 1112 util.set_flags(dst, flags)
1110 1113 cmdutil.addremove(repo, cfiles)
1111 1114 files = patches.keys()
1112 1115 files.extend([r for r in removes if r not in files])
1113 1116 files.sort()
1114 1117
1115 1118 return files
1116 1119
1117 1120 def b85diff(to, tn):
1118 1121 '''print base85-encoded binary diff'''
1119 1122 def gitindex(text):
1120 1123 if not text:
1121 1124 return '0' * 40
1122 1125 l = len(text)
1123 1126 s = sha.new('blob %d\0' % l)
1124 1127 s.update(text)
1125 1128 return s.hexdigest()
1126 1129
1127 1130 def fmtline(line):
1128 1131 l = len(line)
1129 1132 if l <= 26:
1130 1133 l = chr(ord('A') + l - 1)
1131 1134 else:
1132 1135 l = chr(l - 26 + ord('a') - 1)
1133 1136 return '%c%s\n' % (l, base85.b85encode(line, True))
1134 1137
1135 1138 def chunk(text, csize=52):
1136 1139 l = len(text)
1137 1140 i = 0
1138 1141 while i < l:
1139 1142 yield text[i:i+csize]
1140 1143 i += csize
1141 1144
1142 1145 tohash = gitindex(to)
1143 1146 tnhash = gitindex(tn)
1144 1147 if tohash == tnhash:
1145 1148 return ""
1146 1149
1147 1150 # TODO: deltas
1148 1151 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1149 1152 (tohash, tnhash, len(tn))]
1150 1153 for l in chunk(zlib.compress(tn)):
1151 1154 ret.append(fmtline(l))
1152 1155 ret.append('\n')
1153 1156 return ''.join(ret)
1154 1157
1155 1158 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1156 1159 fp=None, changes=None, opts=None):
1157 1160 '''print diff of changes to files between two nodes, or node and
1158 1161 working directory.
1159 1162
1160 1163 if node1 is None, use first dirstate parent instead.
1161 1164 if node2 is None, compare node1 with working directory.'''
1162 1165
1163 1166 if opts is None:
1164 1167 opts = mdiff.defaultopts
1165 1168 if fp is None:
1166 1169 fp = repo.ui
1167 1170
1168 1171 if not node1:
1169 1172 node1 = repo.dirstate.parents()[0]
1170 1173
1171 1174 ccache = {}
1172 1175 def getctx(r):
1173 1176 if r not in ccache:
1174 1177 ccache[r] = context.changectx(repo, r)
1175 1178 return ccache[r]
1176 1179
1177 1180 flcache = {}
1178 1181 def getfilectx(f, ctx):
1179 1182 flctx = ctx.filectx(f, filelog=flcache.get(f))
1180 1183 if f not in flcache:
1181 1184 flcache[f] = flctx._filelog
1182 1185 return flctx
1183 1186
1184 1187 # reading the data for node1 early allows it to play nicely
1185 1188 # with repo.status and the revlog cache.
1186 1189 ctx1 = context.changectx(repo, node1)
1187 1190 # force manifest reading
1188 1191 man1 = ctx1.manifest()
1189 1192 date1 = util.datestr(ctx1.date())
1190 1193
1191 1194 if not changes:
1192 1195 changes = repo.status(node1, node2, files, match=match)[:5]
1193 1196 modified, added, removed, deleted, unknown = changes
1194 1197
1195 1198 if not modified and not added and not removed:
1196 1199 return
1197 1200
1198 1201 if node2:
1199 1202 ctx2 = context.changectx(repo, node2)
1200 1203 execf2 = ctx2.manifest().execf
1201 1204 linkf2 = ctx2.manifest().linkf
1202 1205 else:
1203 1206 ctx2 = context.workingctx(repo)
1204 1207 execf2 = util.execfunc(repo.root, None)
1205 1208 linkf2 = util.linkfunc(repo.root, None)
1206 1209 if execf2 is None:
1207 1210 mc = ctx2.parents()[0].manifest().copy()
1208 1211 execf2 = mc.execf
1209 1212 linkf2 = mc.linkf
1210 1213
1211 1214 if repo.ui.quiet:
1212 1215 r = None
1213 1216 else:
1214 1217 hexfunc = repo.ui.debugflag and hex or short
1215 1218 r = [hexfunc(node) for node in [node1, node2] if node]
1216 1219
1217 1220 if opts.git:
1218 1221 copy, diverge = copies.copies(repo, ctx1, ctx2, repo.changectx(nullid))
1219 1222 for k, v in copy.items():
1220 1223 copy[v] = k
1221 1224
1222 1225 all = modified + added + removed
1223 1226 all.sort()
1224 1227 gone = {}
1225 1228
1226 1229 for f in all:
1227 1230 to = None
1228 1231 tn = None
1229 1232 dodiff = True
1230 1233 header = []
1231 1234 if f in man1:
1232 1235 to = getfilectx(f, ctx1).data()
1233 1236 if f not in removed:
1234 1237 tn = getfilectx(f, ctx2).data()
1235 1238 a, b = f, f
1236 1239 if opts.git:
1237 1240 def gitmode(x, l):
1238 1241 return l and '120000' or (x and '100755' or '100644')
1239 1242 def addmodehdr(header, omode, nmode):
1240 1243 if omode != nmode:
1241 1244 header.append('old mode %s\n' % omode)
1242 1245 header.append('new mode %s\n' % nmode)
1243 1246
1244 1247 if f in added:
1245 1248 mode = gitmode(execf2(f), linkf2(f))
1246 1249 if f in copy:
1247 1250 a = copy[f]
1248 1251 omode = gitmode(man1.execf(a), man1.linkf(a))
1249 1252 addmodehdr(header, omode, mode)
1250 1253 if a in removed and a not in gone:
1251 1254 op = 'rename'
1252 1255 gone[a] = 1
1253 1256 else:
1254 1257 op = 'copy'
1255 1258 header.append('%s from %s\n' % (op, a))
1256 1259 header.append('%s to %s\n' % (op, f))
1257 1260 to = getfilectx(a, ctx1).data()
1258 1261 else:
1259 1262 header.append('new file mode %s\n' % mode)
1260 1263 if util.binary(tn):
1261 1264 dodiff = 'binary'
1262 1265 elif f in removed:
1263 1266 # have we already reported a copy above?
1264 1267 if f in copy and copy[f] in added and copy[copy[f]] == f:
1265 1268 dodiff = False
1266 1269 else:
1267 1270 mode = gitmode(man1.execf(f), man1.linkf(f))
1268 1271 header.append('deleted file mode %s\n' % mode)
1269 1272 else:
1270 1273 omode = gitmode(man1.execf(f), man1.linkf(f))
1271 1274 nmode = gitmode(execf2(f), linkf2(f))
1272 1275 addmodehdr(header, omode, nmode)
1273 1276 if util.binary(to) or util.binary(tn):
1274 1277 dodiff = 'binary'
1275 1278 r = None
1276 1279 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1277 1280 if dodiff:
1278 1281 if dodiff == 'binary':
1279 1282 text = b85diff(to, tn)
1280 1283 else:
1281 1284 text = mdiff.unidiff(to, date1,
1282 1285 # ctx2 date may be dynamic
1283 1286 tn, util.datestr(ctx2.date()),
1284 1287 a, b, r, opts=opts)
1285 1288 if text or len(header) > 1:
1286 1289 fp.write(''.join(header))
1287 1290 fp.write(text)
1288 1291
1289 1292 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1290 1293 opts=None):
1291 1294 '''export changesets as hg patches.'''
1292 1295
1293 1296 total = len(revs)
1294 1297 revwidth = max([len(str(rev)) for rev in revs])
1295 1298
1296 1299 def single(rev, seqno, fp):
1297 1300 ctx = repo.changectx(rev)
1298 1301 node = ctx.node()
1299 1302 parents = [p.node() for p in ctx.parents() if p]
1300 1303 branch = ctx.branch()
1301 1304 if switch_parent:
1302 1305 parents.reverse()
1303 1306 prev = (parents and parents[0]) or nullid
1304 1307
1305 1308 if not fp:
1306 1309 fp = cmdutil.make_file(repo, template, node, total=total,
1307 1310 seqno=seqno, revwidth=revwidth)
1308 1311 if fp != sys.stdout and hasattr(fp, 'name'):
1309 1312 repo.ui.note("%s\n" % fp.name)
1310 1313
1311 1314 fp.write("# HG changeset patch\n")
1312 1315 fp.write("# User %s\n" % ctx.user())
1313 1316 fp.write("# Date %d %d\n" % ctx.date())
1314 1317 if branch and (branch != 'default'):
1315 1318 fp.write("# Branch %s\n" % branch)
1316 1319 fp.write("# Node ID %s\n" % hex(node))
1317 1320 fp.write("# Parent %s\n" % hex(prev))
1318 1321 if len(parents) > 1:
1319 1322 fp.write("# Parent %s\n" % hex(parents[1]))
1320 1323 fp.write(ctx.description().rstrip())
1321 1324 fp.write("\n\n")
1322 1325
1323 1326 diff(repo, prev, node, fp=fp, opts=opts)
1324 1327 if fp not in (sys.stdout, repo.ui):
1325 1328 fp.close()
1326 1329
1327 1330 for seqno, rev in enumerate(revs):
1328 1331 single(rev, seqno+1, fp)
1329 1332
1330 1333 def diffstat(patchlines):
1331 1334 if not util.find_exe('diffstat'):
1332 1335 return
1333 1336 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1334 1337 try:
1335 1338 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1336 1339 try:
1337 1340 for line in patchlines:
1338 1341 p.tochild.write(line + "\n")
1339 1342 p.tochild.close()
1340 1343 if p.wait(): return
1341 1344 fp = os.fdopen(fd, 'r')
1342 1345 stat = []
1343 1346 for line in fp: stat.append(line.lstrip())
1344 1347 last = stat.pop()
1345 1348 stat.insert(0, last)
1346 1349 stat = ''.join(stat)
1347 1350 return stat
1348 1351 except: raise
1349 1352 finally:
1350 1353 try: os.unlink(name)
1351 1354 except: pass
@@ -1,263 +1,275 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 % import of plain diff with specific date and user
36 36 hg clone -r0 a b
37 37 hg --cwd a diff -r0:1 > tip.patch
38 38 hg --cwd b import -mpatch -d '1 0' -u 'user@nowhere.net' ../tip.patch
39 39 hg -R b tip -pv
40 40 rm -r b
41 41
42 42 echo % import of plain diff should be ok with --no-commit
43 43 hg clone -r0 a b
44 44 hg --cwd a diff -r0:1 > tip.patch
45 45 hg --cwd b import --no-commit ../tip.patch
46 46 hg --cwd b diff --nodates
47 47 rm -r b
48 48
49 49 echo % hg -R repo import
50 50 # put the clone in a subdir - having a directory named "a"
51 51 # used to hide a bug.
52 52 mkdir dir
53 53 hg clone -r0 a dir/b
54 54 hg --cwd a export tip > dir/tip.patch
55 55 cd dir
56 56 hg -R b import tip.patch
57 57 cd ..
58 58 rm -r dir
59 59
60 60 echo % import from stdin
61 61 hg clone -r0 a b
62 62 hg --cwd a export tip | hg --cwd b import -
63 63 rm -r b
64 64
65 65 echo % override commit message
66 66 hg clone -r0 a b
67 67 hg --cwd a export tip | hg --cwd b import -m 'override' -
68 68 hg --cwd b tip | grep override
69 69 rm -r b
70 70
71 71 cat > mkmsg.py <<EOF
72 72 import email.Message, sys
73 73 msg = email.Message.Message()
74 74 msg.set_payload('email commit message\n' + open('tip.patch', 'rb').read())
75 75 msg['Subject'] = 'email patch'
76 76 msg['From'] = 'email patcher'
77 77 sys.stdout.write(msg.as_string())
78 78 EOF
79 79
80 80 echo % plain diff in email, subject, message body
81 81 hg clone -r0 a b
82 82 hg --cwd a diff -r0:1 > tip.patch
83 83 python mkmsg.py > msg.patch
84 84 hg --cwd b import ../msg.patch
85 85 hg --cwd b tip | grep email
86 86 rm -r b
87 87
88 88 echo % plain diff in email, no subject, message body
89 89 hg clone -r0 a b
90 90 grep -v '^Subject:' msg.patch | hg --cwd b import -
91 91 rm -r b
92 92
93 93 echo % plain diff in email, subject, no message body
94 94 hg clone -r0 a b
95 95 grep -v '^email ' msg.patch | hg --cwd b import -
96 96 rm -r b
97 97
98 98 echo % plain diff in email, no subject, no message body, should fail
99 99 hg clone -r0 a b
100 100 egrep -v '^(Subject|email)' msg.patch | hg --cwd b import -
101 101 rm -r b
102 102
103 103 echo % hg export in email, should use patch header
104 104 hg clone -r0 a b
105 105 hg --cwd a export tip > tip.patch
106 106 python mkmsg.py | hg --cwd b import -
107 107 hg --cwd b tip | grep second
108 108 rm -r b
109 109
110 110 # subject: duplicate detection, removal of [PATCH]
111 111 # The '---' tests the gitsendmail handling without proper mail headers
112 112 cat > mkmsg2.py <<EOF
113 113 import email.Message, sys
114 114 msg = email.Message.Message()
115 115 msg.set_payload('email patch\n\nnext line\n---\n' + open('tip.patch').read())
116 116 msg['Subject'] = '[PATCH] email patch'
117 117 msg['From'] = 'email patcher'
118 118 sys.stdout.write(msg.as_string())
119 119 EOF
120 120
121 121 echo '% plain diff in email, [PATCH] subject, message body with subject'
122 122 hg clone -r0 a b
123 123 hg --cwd a diff -r0:1 > tip.patch
124 124 python mkmsg2.py | hg --cwd b import -
125 125 hg --cwd b tip --template '{desc}\n'
126 126 rm -r b
127 127
128 128 # We weren't backing up the correct dirstate file when importing many patches
129 129 # (issue963)
130 130 echo '% import patch1 patch2; rollback'
131 131 echo line 3 >> a/a
132 132 hg --cwd a ci -m'third change'
133 133 hg --cwd a export -o '../patch%R' 1 2
134 134 hg clone -qr0 a b
135 135 hg --cwd b parents --template 'parent: #rev#\n'
136 136 hg --cwd b import ../patch1 ../patch2
137 137 hg --cwd b rollback
138 138 hg --cwd b parents --template 'parent: #rev#\n'
139 139 rm -r b
140 140
141 141 # bug non regression test
142 142 # importing a patch in a subdirectory failed at the commit stage
143 143 echo line 2 >> a/d1/d2/a
144 144 hg --cwd a ci -u someoneelse -d '1 0' -m'subdir change'
145 145 echo % hg import in a subdirectory
146 146 hg clone -r0 a b
147 147 hg --cwd a export tip | sed -e 's/d1\/d2\///' > tip.patch
148 148 dir=`pwd`
149 149 cd b/d1/d2 2>&1 > /dev/null
150 150 hg import ../../../tip.patch
151 151 cd $dir
152 152 echo "% message should be 'subdir change'"
153 153 hg --cwd b tip | grep 'subdir change'
154 154 echo "% committer should be 'someoneelse'"
155 155 hg --cwd b tip | grep someoneelse
156 156 echo "% should be empty"
157 157 hg --cwd b status
158 158
159 159
160 160 # Test fuzziness (ambiguous patch location, fuzz=2)
161 161 echo % test fuzziness
162 162 hg init fuzzy
163 163 cd fuzzy
164 164 echo line1 > a
165 165 echo line0 >> a
166 166 echo line3 >> a
167 167 hg ci -Am adda
168 168 echo line1 > a
169 169 echo line2 >> a
170 170 echo line0 >> a
171 171 echo line3 >> a
172 172 hg ci -m change a
173 173 hg export tip > tip.patch
174 174 hg up -C 0
175 175 echo line1 > a
176 176 echo line0 >> a
177 177 echo line1 >> a
178 178 echo line0 >> a
179 179 hg ci -m brancha
180 180 hg import -v tip.patch
181 181 cd ..
182 182
183 183 # Test hunk touching empty files (issue906)
184 184 hg init empty
185 185 cd empty
186 186 touch a
187 187 touch b1
188 188 touch c1
189 189 echo d > d
190 190 hg ci -Am init
191 191 echo a > a
192 192 echo b > b1
193 193 hg mv b1 b2
194 194 echo c > c1
195 195 hg copy c1 c2
196 196 rm d
197 197 touch d
198 198 hg diff --git
199 199 hg ci -m empty
200 200 hg export --git tip > empty.diff
201 201 hg up -C 0
202 202 hg import empty.diff
203 203 for name in a b1 b2 c1 c2 d;
204 204 do
205 205 echo % $name file
206 206 test -f $name && cat $name
207 207 done
208 208 cd ..
209 209
210 210 # Test importing a patch ending with a binary file removal
211 211 echo % test trailing binary removal
212 212 hg init binaryremoval
213 213 cd binaryremoval
214 214 echo a > a
215 215 python -c "file('b', 'wb').write('a\x00b')"
216 216 hg ci -Am addall
217 217 hg rm a
218 218 hg rm b
219 219 hg st
220 220 hg ci -m remove
221 221 hg export --git . > remove.diff
222 222 cat remove.diff | grep git
223 223 hg up -C 0
224 224 hg import remove.diff
225 225 hg manifest
226 226 cd ..
227 227
228 228 echo % 'test update+rename with common name (issue 927)'
229 229 hg init t
230 230 cd t
231 231 touch a
232 232 hg ci -Am t
233 233 echo a > a
234 234 # Here, bfile.startswith(afile)
235 235 hg copy a a2
236 236 hg ci -m copya
237 237 hg export --git tip > copy.diff
238 238 hg up -C 0
239 239 hg import copy.diff
240 240 echo % view a
241 241 # a should contain an 'a'
242 242 cat a
243 243 echo % view a2
244 244 # and a2 should have duplicated it
245 245 cat a2
246 246 cd ..
247 247
248 248 echo % 'test -p0'
249 249 hg init p0
250 250 cd p0
251 251 echo a > a
252 252 hg ci -Am t
253 253 hg import -p0 - << EOF
254 254 foobar
255 255 --- a Sat Apr 12 22:43:58 2008 -0400
256 256 +++ a Sat Apr 12 22:44:05 2008 -0400
257 257 @@ -1,1 +1,1 @@
258 258 -a
259 259 +bb
260 260 EOF
261 261 hg status
262 262 cat a
263 263 cd ..
264
265 echo % 'test paths outside repo root'
266 mkdir outside
267 touch outside/foo
268 hg init inside
269 cd inside
270 hg import - <<EOF
271 diff --git a/a b/b
272 rename from ../outside/foo
273 rename to bar
274 EOF
275 cd ..
@@ -1,262 +1,265 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 updating working directory
10 10 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
11 11 applying ../tip.patch
12 12 % message should be same
13 13 summary: second change
14 14 % committer should be same
15 15 user: someone
16 16 % import of plain diff should fail without message
17 17 requesting all changes
18 18 adding changesets
19 19 adding manifests
20 20 adding file changes
21 21 added 1 changesets with 2 changes to 2 files
22 22 updating working directory
23 23 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
24 24 applying ../tip.patch
25 25 transaction abort!
26 26 rollback completed
27 27 abort: empty commit message
28 28 % import of plain diff should be ok with message
29 29 requesting all changes
30 30 adding changesets
31 31 adding manifests
32 32 adding file changes
33 33 added 1 changesets with 2 changes to 2 files
34 34 updating working directory
35 35 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
36 36 applying ../tip.patch
37 37 % import of plain diff with specific date and user
38 38 requesting all changes
39 39 adding changesets
40 40 adding manifests
41 41 adding file changes
42 42 added 1 changesets with 2 changes to 2 files
43 43 updating working directory
44 44 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
45 45 applying ../tip.patch
46 46 changeset: 1:ca68f19f3a40
47 47 tag: tip
48 48 user: user@nowhere.net
49 49 date: Thu Jan 01 00:00:01 1970 +0000
50 50 files: a
51 51 description:
52 52 patch
53 53
54 54
55 55 diff -r 80971e65b431 -r ca68f19f3a40 a
56 56 --- a/a Thu Jan 01 00:00:00 1970 +0000
57 57 +++ b/a Thu Jan 01 00:00:01 1970 +0000
58 58 @@ -1,1 +1,2 @@
59 59 line 1
60 60 +line 2
61 61
62 62 % import of plain diff should be ok with --no-commit
63 63 requesting all changes
64 64 adding changesets
65 65 adding manifests
66 66 adding file changes
67 67 added 1 changesets with 2 changes to 2 files
68 68 updating working directory
69 69 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
70 70 applying ../tip.patch
71 71 diff -r 80971e65b431 a
72 72 --- a/a
73 73 +++ b/a
74 74 @@ -1,1 +1,2 @@
75 75 line 1
76 76 +line 2
77 77 % hg -R repo import
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 updating working directory
84 84 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
85 85 applying tip.patch
86 86 % import from stdin
87 87 requesting all changes
88 88 adding changesets
89 89 adding manifests
90 90 adding file changes
91 91 added 1 changesets with 2 changes to 2 files
92 92 updating working directory
93 93 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
94 94 applying patch from stdin
95 95 % override commit message
96 96 requesting all changes
97 97 adding changesets
98 98 adding manifests
99 99 adding file changes
100 100 added 1 changesets with 2 changes to 2 files
101 101 updating working directory
102 102 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
103 103 applying patch from stdin
104 104 summary: override
105 105 % plain diff in email, subject, message body
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 updating working directory
112 112 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
113 113 applying ../msg.patch
114 114 user: email patcher
115 115 summary: email patch
116 116 % plain diff in email, no subject, message body
117 117 requesting all changes
118 118 adding changesets
119 119 adding manifests
120 120 adding file changes
121 121 added 1 changesets with 2 changes to 2 files
122 122 updating working directory
123 123 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
124 124 applying patch from stdin
125 125 % plain diff in email, subject, no message body
126 126 requesting all changes
127 127 adding changesets
128 128 adding manifests
129 129 adding file changes
130 130 added 1 changesets with 2 changes to 2 files
131 131 updating working directory
132 132 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
133 133 applying patch from stdin
134 134 % plain diff in email, no subject, no message body, should fail
135 135 requesting all changes
136 136 adding changesets
137 137 adding manifests
138 138 adding file changes
139 139 added 1 changesets with 2 changes to 2 files
140 140 updating working directory
141 141 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
142 142 applying patch from stdin
143 143 transaction abort!
144 144 rollback completed
145 145 abort: empty commit message
146 146 % hg export in email, should use patch header
147 147 requesting all changes
148 148 adding changesets
149 149 adding manifests
150 150 adding file changes
151 151 added 1 changesets with 2 changes to 2 files
152 152 updating working directory
153 153 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
154 154 applying patch from stdin
155 155 summary: second change
156 156 % plain diff in email, [PATCH] subject, message body with subject
157 157 requesting all changes
158 158 adding changesets
159 159 adding manifests
160 160 adding file changes
161 161 added 1 changesets with 2 changes to 2 files
162 162 updating working directory
163 163 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
164 164 applying patch from stdin
165 165 email patch
166 166
167 167 next line
168 168 ---
169 169 % import patch1 patch2; rollback
170 170 parent: 0
171 171 applying ../patch1
172 172 applying ../patch2
173 173 rolling back last transaction
174 174 parent: 1
175 175 % hg import in a subdirectory
176 176 requesting all changes
177 177 adding changesets
178 178 adding manifests
179 179 adding file changes
180 180 added 1 changesets with 2 changes to 2 files
181 181 updating working directory
182 182 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
183 183 applying ../../../tip.patch
184 184 % message should be 'subdir change'
185 185 summary: subdir change
186 186 % committer should be 'someoneelse'
187 187 user: someoneelse
188 188 % should be empty
189 189 % test fuzziness
190 190 adding a
191 191 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
192 192 created new head
193 193 applying tip.patch
194 194 patching file a
195 195 Hunk #1 succeeded at 1 with fuzz 2 (offset -2 lines).
196 196 a
197 197 adding a
198 198 adding b1
199 199 adding c1
200 200 adding d
201 201 diff --git a/a b/a
202 202 --- a/a
203 203 +++ b/a
204 204 @@ -0,0 +1,1 @@
205 205 +a
206 206 diff --git a/b1 b/b2
207 207 rename from b1
208 208 rename to b2
209 209 --- a/b1
210 210 +++ b/b2
211 211 @@ -0,0 +1,1 @@
212 212 +b
213 213 diff --git a/c1 b/c1
214 214 --- a/c1
215 215 +++ b/c1
216 216 @@ -0,0 +1,1 @@
217 217 +c
218 218 diff --git a/c1 b/c2
219 219 copy from c1
220 220 copy to c2
221 221 --- a/c1
222 222 +++ b/c2
223 223 @@ -0,0 +1,1 @@
224 224 +c
225 225 diff --git a/d b/d
226 226 --- a/d
227 227 +++ b/d
228 228 @@ -1,1 +0,0 @@
229 229 -d
230 230 4 files updated, 0 files merged, 2 files removed, 0 files unresolved
231 231 applying empty.diff
232 232 % a file
233 233 a
234 234 % b1 file
235 235 % b2 file
236 236 b
237 237 % c1 file
238 238 c
239 239 % c2 file
240 240 c
241 241 % d file
242 242 % test trailing binary removal
243 243 adding a
244 244 adding b
245 245 R a
246 246 R b
247 247 diff --git a/a b/a
248 248 diff --git a/b b/b
249 249 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
250 250 applying remove.diff
251 251 % test update+rename with common name (issue 927)
252 252 adding a
253 253 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
254 254 applying copy.diff
255 255 % view a
256 256 a
257 257 % view a2
258 258 a
259 259 % test -p0
260 260 adding a
261 261 applying patch from stdin
262 262 bb
263 % test paths outside repo root
264 applying patch from stdin
265 abort: ../outside/foo not under root
General Comments 0
You need to be logged in to leave comments. Login now