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