##// END OF EJS Templates
diffstat: don't fail on merges...
Patrick Mezard -
r7664:3cc74ee7 default
parent child Browse files
Show More
@@ -1,1398 +1,1398
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 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
57 57 '(---|\*\*\*)[ \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 754 gooda = not nulla and os.path.exists(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')
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 not match:
1205 1205 match = cmdutil.matchall(repo)
1206 1206
1207 1207 if opts is None:
1208 1208 opts = mdiff.defaultopts
1209 1209
1210 1210 if not node1:
1211 1211 node1 = repo.dirstate.parents()[0]
1212 1212
1213 1213 flcache = {}
1214 1214 def getfilectx(f, ctx):
1215 1215 flctx = ctx.filectx(f, filelog=flcache.get(f))
1216 1216 if f not in flcache:
1217 1217 flcache[f] = flctx._filelog
1218 1218 return flctx
1219 1219
1220 1220 ctx1 = repo[node1]
1221 1221 ctx2 = repo[node2]
1222 1222
1223 1223 if not changes:
1224 1224 changes = repo.status(ctx1, ctx2, match=match)
1225 1225 modified, added, removed = changes[:3]
1226 1226
1227 1227 if not modified and not added and not removed:
1228 1228 return
1229 1229
1230 1230 date1 = util.datestr(ctx1.date())
1231 1231 man1 = ctx1.manifest()
1232 1232
1233 1233 if repo.ui.quiet:
1234 1234 r = None
1235 1235 else:
1236 1236 hexfunc = repo.ui.debugflag and hex or short
1237 1237 r = [hexfunc(node) for node in [node1, node2] if node]
1238 1238
1239 1239 if opts.git:
1240 1240 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
1241 1241 for k, v in copy.items():
1242 1242 copy[v] = k
1243 1243
1244 1244 gone = {}
1245 1245 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1246 1246
1247 1247 for f in util.sort(modified + added + removed):
1248 1248 to = None
1249 1249 tn = None
1250 1250 dodiff = True
1251 1251 header = []
1252 1252 if f in man1:
1253 1253 to = getfilectx(f, ctx1).data()
1254 1254 if f not in removed:
1255 1255 tn = getfilectx(f, ctx2).data()
1256 1256 a, b = f, f
1257 1257 if opts.git:
1258 1258 if f in added:
1259 1259 mode = gitmode[ctx2.flags(f)]
1260 1260 if f in copy:
1261 1261 a = copy[f]
1262 1262 omode = gitmode[man1.flags(a)]
1263 1263 _addmodehdr(header, omode, mode)
1264 1264 if a in removed and a not in gone:
1265 1265 op = 'rename'
1266 1266 gone[a] = 1
1267 1267 else:
1268 1268 op = 'copy'
1269 1269 header.append('%s from %s\n' % (op, a))
1270 1270 header.append('%s to %s\n' % (op, f))
1271 1271 to = getfilectx(a, ctx1).data()
1272 1272 else:
1273 1273 header.append('new file mode %s\n' % mode)
1274 1274 if util.binary(tn):
1275 1275 dodiff = 'binary'
1276 1276 elif f in removed:
1277 1277 # have we already reported a copy above?
1278 1278 if f in copy and copy[f] in added and copy[copy[f]] == f:
1279 1279 dodiff = False
1280 1280 else:
1281 1281 header.append('deleted file mode %s\n' %
1282 1282 gitmode[man1.flags(f)])
1283 1283 else:
1284 1284 omode = gitmode[man1.flags(f)]
1285 1285 nmode = gitmode[ctx2.flags(f)]
1286 1286 _addmodehdr(header, omode, nmode)
1287 1287 if util.binary(to) or util.binary(tn):
1288 1288 dodiff = 'binary'
1289 1289 r = None
1290 1290 header.insert(0, mdiff.diffline(r, a, b, opts))
1291 1291 if dodiff:
1292 1292 if dodiff == 'binary':
1293 1293 text = b85diff(to, tn)
1294 1294 else:
1295 1295 text = mdiff.unidiff(to, date1,
1296 1296 # ctx2 date may be dynamic
1297 1297 tn, util.datestr(ctx2.date()),
1298 1298 a, b, r, opts=opts)
1299 1299 if header and (text or len(header) > 1):
1300 1300 yield ''.join(header)
1301 1301 if text:
1302 1302 yield text
1303 1303
1304 1304 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1305 1305 opts=None):
1306 1306 '''export changesets as hg patches.'''
1307 1307
1308 1308 total = len(revs)
1309 1309 revwidth = max([len(str(rev)) for rev in revs])
1310 1310
1311 1311 def single(rev, seqno, fp):
1312 1312 ctx = repo[rev]
1313 1313 node = ctx.node()
1314 1314 parents = [p.node() for p in ctx.parents() if p]
1315 1315 branch = ctx.branch()
1316 1316 if switch_parent:
1317 1317 parents.reverse()
1318 1318 prev = (parents and parents[0]) or nullid
1319 1319
1320 1320 if not fp:
1321 1321 fp = cmdutil.make_file(repo, template, node, total=total,
1322 1322 seqno=seqno, revwidth=revwidth,
1323 1323 mode='ab')
1324 1324 if fp != sys.stdout and hasattr(fp, 'name'):
1325 1325 repo.ui.note("%s\n" % fp.name)
1326 1326
1327 1327 fp.write("# HG changeset patch\n")
1328 1328 fp.write("# User %s\n" % ctx.user())
1329 1329 fp.write("# Date %d %d\n" % ctx.date())
1330 1330 if branch and (branch != 'default'):
1331 1331 fp.write("# Branch %s\n" % branch)
1332 1332 fp.write("# Node ID %s\n" % hex(node))
1333 1333 fp.write("# Parent %s\n" % hex(prev))
1334 1334 if len(parents) > 1:
1335 1335 fp.write("# Parent %s\n" % hex(parents[1]))
1336 1336 fp.write(ctx.description().rstrip())
1337 1337 fp.write("\n\n")
1338 1338
1339 1339 for chunk in diff(repo, prev, node, opts=opts):
1340 1340 fp.write(chunk)
1341 1341
1342 1342 for seqno, rev in enumerate(revs):
1343 1343 single(rev, seqno+1, fp)
1344 1344
1345 1345 def diffstatdata(lines):
1346 filename = None
1346 filename, adds, removes = None, 0, 0
1347 1347 for line in lines:
1348 1348 if line.startswith('diff'):
1349 1349 if filename:
1350 1350 yield (filename, adds, removes)
1351 1351 # set numbers to 0 anyway when starting new file
1352 adds = 0
1353 removes = 0
1352 adds, removes = 0, 0
1354 1353 if line.startswith('diff --git'):
1355 1354 filename = gitre.search(line).group(1)
1356 1355 else:
1357 1356 # format: "diff -r ... -r ... file name"
1358 1357 filename = line.split(None, 5)[-1]
1359 1358 elif line.startswith('+') and not line.startswith('+++'):
1360 1359 adds += 1
1361 1360 elif line.startswith('-') and not line.startswith('---'):
1362 1361 removes += 1
1362 if filename:
1363 1363 yield (filename, adds, removes)
1364 1364
1365 1365 def diffstat(lines):
1366 1366 output = []
1367 1367 stats = list(diffstatdata(lines))
1368 1368 width = util.termwidth() - 2
1369 1369
1370 1370 maxtotal, maxname = 0, 0
1371 1371 totaladds, totalremoves = 0, 0
1372 1372 for filename, adds, removes in stats:
1373 1373 totaladds += adds
1374 1374 totalremoves += removes
1375 1375 maxname = max(maxname, len(filename))
1376 1376 maxtotal = max(maxtotal, adds+removes)
1377 1377
1378 1378 countwidth = len(str(maxtotal))
1379 1379 graphwidth = width - countwidth - maxname
1380 1380 if graphwidth < 10:
1381 1381 graphwidth = 10
1382 1382
1383 1383 factor = int(math.ceil(float(maxtotal) / graphwidth))
1384 1384
1385 1385 for filename, adds, removes in stats:
1386 1386 # If diffstat runs out of room it doesn't print anything, which
1387 1387 # isn't very useful, so always print at least one + or - if there
1388 1388 # were at least some changes
1389 1389 pluses = '+' * max(adds/factor, int(bool(adds)))
1390 1390 minuses = '-' * max(removes/factor, int(bool(removes)))
1391 1391 output.append(' %-*s | %*.d %s%s\n' % (maxname, filename, countwidth,
1392 1392 adds+removes, pluses, minuses))
1393 1393
1394 1394 if stats:
1395 1395 output.append(' %d files changed, %d insertions(+), %d deletions(-)\n' %
1396 1396 (len(stats), totaladds, totalremoves))
1397 1397
1398 1398 return ''.join(output)
@@ -1,86 +1,100
1 1 #!/bin/sh
2 2
3 3 cat <<EOF >> $HGRCPATH
4 4 [extensions]
5 5 notify=
6 6
7 7 [hooks]
8 8 incoming.notify = python:hgext.notify.hook
9 9
10 10 [notify]
11 11 sources = pull
12 12 diffstat = False
13 13
14 14 [usersubs]
15 15 foo@bar = *
16 16
17 17 [reposubs]
18 18 * = baz
19 19 EOF
20 20
21 21 hg help notify
22 22 hg init a
23 23 echo a > a/a
24 24 echo % commit
25 25 hg --traceback --cwd a commit -Ama -d '0 0'
26 26
27 27 echo % clone
28 28 hg --traceback clone a b
29 29
30 30 echo a >> a/a
31 31 echo % commit
32 32 hg --traceback --cwd a commit -Amb -d '1 0'
33 33
34 34 # on Mac OS X 10.5 the tmp path is very long so would get stripped in the subject line
35 35 cat <<EOF >> $HGRCPATH
36 36 [notify]
37 37 maxsubject = 200
38 38 EOF
39 39
40 40 # the python call below wraps continuation lines, which appear on Mac OS X 10.5 because
41 41 # of the very long subject line
42 42 echo '% pull (minimal config)'
43 43 hg --traceback --cwd b pull ../a 2>&1 |
44 44 python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),' |
45 45 sed -e 's/\(Message-Id:\).*/\1/' \
46 46 -e 's/changeset \([0-9a-f]* *\)in .*test-notif/changeset \1in test-notif/' \
47 47 -e 's/^details: .*test-notify/details: test-notify/' \
48 48 -e 's/^Date:.*/Date:/'
49 49
50 50 cat <<EOF >> $HGRCPATH
51 51 [notify]
52 52 config = $HGTMP/.notify.conf
53 53 domain = test.com
54 54 strip = 3
55 55 template = Subject: {desc|firstline|strip}\nFrom: {author}\nX-Test: foo\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
56 56
57 57 [web]
58 58 baseurl = http://test/
59 59 EOF
60 60
61 61 echo % fail for config file is missing
62 62 hg --cwd b rollback
63 63 hg --cwd b pull ../a 2>&1 | grep 'unable to open.*\.notify\.conf' > /dev/null && echo pull failed
64 64
65 65 touch "$HGTMP/.notify.conf"
66 66
67 67 echo % pull
68 68 hg --cwd b rollback
69 69 hg --traceback --cwd b pull ../a 2>&1 | sed -e 's/\(Message-Id:\).*/\1/' \
70 70 -e 's/changeset \([0-9a-f]*\) in .*/changeset \1/' \
71 71 -e 's/^Date:.*/Date:/'
72 72
73 73 cat << EOF >> $HGRCPATH
74 74 [hooks]
75 75 incoming.notify = python:hgext.notify.hook
76 76
77 77 [notify]
78 78 sources = pull
79 79 diffstat = True
80 80 EOF
81 81
82 82 echo % pull
83 83 hg --cwd b rollback
84 84 hg --traceback --cwd b pull ../a 2>&1 | sed -e 's/\(Message-Id:\).*/\1/' \
85 85 -e 's/changeset \([0-9a-f]*\) in .*/changeset \1/' \
86 86 -e 's/^Date:.*/Date:/'
87
88 echo % test merge
89 cd a
90 hg up -C 0
91 echo a >> a
92 hg ci -Am adda2 -d '2 0'
93 hg merge
94 hg ci -m merge -d '3 0'
95 cd ..
96
97 hg --traceback --cwd b pull ../a 2>&1 | sed -e 's/\(Message-Id:\).*/\1/' \
98 -e 's/changeset \([0-9a-f]*\) in .*/changeset \1/' \
99 -e 's/^Date:.*/Date:/'
100
@@ -1,166 +1,219
1 1 notify extension - hook extension to email notifications on commits/pushes
2 2
3 3 Subscriptions can be managed through hgrc. Default mode is to print
4 4 messages to stdout, for testing and configuring.
5 5
6 6 To use, configure notify extension and enable in hgrc like this:
7 7
8 8 [extensions]
9 9 hgext.notify =
10 10
11 11 [hooks]
12 12 # one email for each incoming changeset
13 13 incoming.notify = python:hgext.notify.hook
14 14 # batch emails when many changesets incoming at one time
15 15 changegroup.notify = python:hgext.notify.hook
16 16
17 17 [notify]
18 18 # config items go in here
19 19
20 20 config items:
21 21
22 22 REQUIRED:
23 23 config = /path/to/file # file containing subscriptions
24 24
25 25 OPTIONAL:
26 26 test = True # print messages to stdout for testing
27 27 strip = 3 # number of slashes to strip for url paths
28 28 domain = example.com # domain to use if committer missing domain
29 29 style = ... # style file to use when formatting email
30 30 template = ... # template to use when formatting email
31 31 incoming = ... # template to use when run as incoming hook
32 32 changegroup = ... # template when run as changegroup hook
33 33 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
34 34 maxsubject = 67 # truncate subject line longer than this
35 35 diffstat = True # add a diffstat before the diff content
36 36 sources = serve # notify if source of incoming changes in this list
37 37 # (serve == ssh or http, push, pull, bundle)
38 38 [email]
39 39 from = user@host.com # email address to send as if none given
40 40 [web]
41 41 baseurl = http://hgserver/... # root of hg web site for browsing commits
42 42
43 43 notify config file has same format as regular hgrc. it has two
44 44 sections so you can express subscriptions in whatever way is handier
45 45 for you.
46 46
47 47 [usersubs]
48 48 # key is subscriber email, value is ","-separated list of glob patterns
49 49 user@host = pattern
50 50
51 51 [reposubs]
52 52 # key is glob pattern, value is ","-separated list of subscriber emails
53 53 pattern = user@host
54 54
55 55 glob patterns are matched against path to repo root.
56 56
57 57 if you like, you can put notify config file in repo that users can
58 58 push changes to, they can manage their own subscriptions.
59 59
60 60 no commands defined
61 61 % commit
62 62 adding a
63 63 % clone
64 64 updating working directory
65 65 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
66 66 % commit
67 67 % pull (minimal config)
68 68 pulling from ../a
69 69 searching for changes
70 70 adding changesets
71 71 adding manifests
72 72 adding file changes
73 73 added 1 changesets with 1 changes to 1 files
74 74 Content-Type: text/plain; charset="us-ascii"
75 75 MIME-Version: 1.0
76 76 Content-Transfer-Encoding: 7bit
77 77 Date:
78 78 Subject: changeset in test-notify/b: b
79 79 From: test
80 80 X-Hg-Notification: changeset 0647d048b600
81 81 Message-Id:
82 82 To: baz, foo@bar
83 83
84 84 changeset 0647d048b600 in test-notify/b
85 85 details: test-notify/b?cmd=changeset;node=0647d048b600
86 86 description: b
87 87
88 88 diffs (6 lines):
89 89
90 90 diff -r cb9a9f314b8b -r 0647d048b600 a
91 91 --- a/a Thu Jan 01 00:00:00 1970 +0000
92 92 +++ b/a Thu Jan 01 00:00:01 1970 +0000
93 93 @@ -1,1 +1,2 @@
94 94 a
95 95 +a
96 96 (run 'hg update' to get a working copy)
97 97 % fail for config file is missing
98 98 rolling back last transaction
99 99 pull failed
100 100 % pull
101 101 rolling back last transaction
102 102 pulling from ../a
103 103 searching for changes
104 104 adding changesets
105 105 adding manifests
106 106 adding file changes
107 107 added 1 changesets with 1 changes to 1 files
108 108 Content-Type: text/plain; charset="us-ascii"
109 109 MIME-Version: 1.0
110 110 Content-Transfer-Encoding: 7bit
111 111 X-Test: foo
112 112 Date:
113 113 Subject: b
114 114 From: test@test.com
115 115 X-Hg-Notification: changeset 0647d048b600
116 116 Message-Id:
117 117 To: baz@test.com, foo@bar
118 118
119 119 changeset 0647d048b600
120 120 description:
121 121 b
122 122 diffs (6 lines):
123 123
124 124 diff -r cb9a9f314b8b -r 0647d048b600 a
125 125 --- a/a Thu Jan 01 00:00:00 1970 +0000
126 126 +++ b/a Thu Jan 01 00:00:01 1970 +0000
127 127 @@ -1,1 +1,2 @@
128 128 a
129 129 +a
130 130 (run 'hg update' to get a working copy)
131 131 % pull
132 132 rolling back last transaction
133 133 pulling from ../a
134 134 searching for changes
135 135 adding changesets
136 136 adding manifests
137 137 adding file changes
138 138 added 1 changesets with 1 changes to 1 files
139 139 Content-Type: text/plain; charset="us-ascii"
140 140 MIME-Version: 1.0
141 141 Content-Transfer-Encoding: 7bit
142 142 X-Test: foo
143 143 Date:
144 144 Subject: b
145 145 From: test@test.com
146 146 X-Hg-Notification: changeset 0647d048b600
147 147 Message-Id:
148 148 To: baz@test.com, foo@bar
149 149
150 150 changeset 0647d048b600
151 151 description:
152 152 b
153 153 diffstat:
154 154
155 155 a | 1 +
156 156 1 files changed, 1 insertions(+), 0 deletions(-)
157 157
158 158 diffs (6 lines):
159 159
160 160 diff -r cb9a9f314b8b -r 0647d048b600 a
161 161 --- a/a Thu Jan 01 00:00:00 1970 +0000
162 162 +++ b/a Thu Jan 01 00:00:01 1970 +0000
163 163 @@ -1,1 +1,2 @@
164 164 a
165 165 +a
166 166 (run 'hg update' to get a working copy)
167 % test merge
168 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
169 created new head
170 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
171 (branch merge, don't forget to commit)
172 pulling from ../a
173 searching for changes
174 adding changesets
175 adding manifests
176 adding file changes
177 added 2 changesets with 0 changes to 1 files
178 Content-Type: text/plain; charset="us-ascii"
179 MIME-Version: 1.0
180 Content-Transfer-Encoding: 7bit
181 X-Test: foo
182 Date:
183 Subject: adda2
184 From: test@test.com
185 X-Hg-Notification: changeset 0a184ce6067f
186 Message-Id:
187 To: baz@test.com, foo@bar
188
189 changeset 0a184ce6067f
190 description:
191 adda2
192 diffstat:
193
194 a | 1 +
195 1 files changed, 1 insertions(+), 0 deletions(-)
196
197 diffs (6 lines):
198
199 diff -r cb9a9f314b8b -r 0a184ce6067f a
200 --- a/a Thu Jan 01 00:00:00 1970 +0000
201 +++ b/a Thu Jan 01 00:00:02 1970 +0000
202 @@ -1,1 +1,2 @@
203 a
204 +a
205 Content-Type: text/plain; charset="us-ascii"
206 MIME-Version: 1.0
207 Content-Transfer-Encoding: 7bit
208 X-Test: foo
209 Date:
210 Subject: merge
211 From: test@test.com
212 X-Hg-Notification: changeset 22c88b85aa27
213 Message-Id:
214 To: baz@test.com, foo@bar
215
216 changeset 22c88b85aa27
217 description:
218 merge
219 (run 'hg update' to get a working copy)
General Comments 0
You need to be logged in to leave comments. Login now