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