##// END OF EJS Templates
Merge with crew-stable
Patrick Mezard -
r9591:012f1244 merge default
parent child Browse files
Show More
@@ -1,1424 +1,1428
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 of the
7 7 # GNU General Public License version 2, 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, diffhelpers, copies
12 12 import cStringIO, email.Parser, os, re
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 if not subject and not user:
67 # Not an email, restore parsed headers if any
68 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
69
66 70 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
67 71 # should try to parse msg['Date']
68 72 date = None
69 73 nodeid = None
70 74 branch = None
71 75 parents = []
72 76
73 77 if subject:
74 78 if subject.startswith('[PATCH'):
75 79 pend = subject.find(']')
76 80 if pend >= 0:
77 81 subject = subject[pend+1:].lstrip()
78 82 subject = subject.replace('\n\t', ' ')
79 83 ui.debug('Subject: %s\n' % subject)
80 84 if user:
81 85 ui.debug('From: %s\n' % user)
82 86 diffs_seen = 0
83 87 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
84 88 message = ''
85 89 for part in msg.walk():
86 90 content_type = part.get_content_type()
87 91 ui.debug('Content-Type: %s\n' % content_type)
88 92 if content_type not in ok_types:
89 93 continue
90 94 payload = part.get_payload(decode=True)
91 95 m = diffre.search(payload)
92 96 if m:
93 97 hgpatch = False
94 98 ignoretext = False
95 99
96 100 ui.debug('found patch at byte %d\n' % m.start(0))
97 101 diffs_seen += 1
98 102 cfp = cStringIO.StringIO()
99 103 for line in payload[:m.start(0)].splitlines():
100 104 if line.startswith('# HG changeset patch'):
101 105 ui.debug('patch generated by hg export\n')
102 106 hgpatch = True
103 107 # drop earlier commit message content
104 108 cfp.seek(0)
105 109 cfp.truncate()
106 110 subject = None
107 111 elif hgpatch:
108 112 if line.startswith('# User '):
109 113 user = line[7:]
110 114 ui.debug('From: %s\n' % user)
111 115 elif line.startswith("# Date "):
112 116 date = line[7:]
113 117 elif line.startswith("# Branch "):
114 118 branch = line[9:]
115 119 elif line.startswith("# Node ID "):
116 120 nodeid = line[10:]
117 121 elif line.startswith("# Parent "):
118 122 parents.append(line[10:])
119 123 elif line == '---' and gitsendmail:
120 124 ignoretext = True
121 125 if not line.startswith('# ') and not ignoretext:
122 126 cfp.write(line)
123 127 cfp.write('\n')
124 128 message = cfp.getvalue()
125 129 if tmpfp:
126 130 tmpfp.write(payload)
127 131 if not payload.endswith('\n'):
128 132 tmpfp.write('\n')
129 133 elif not diffs_seen and message and content_type == 'text/plain':
130 134 message += '\n' + payload
131 135 except:
132 136 tmpfp.close()
133 137 os.unlink(tmpname)
134 138 raise
135 139
136 140 if subject and not message.startswith(subject):
137 141 message = '%s\n%s' % (subject, message)
138 142 tmpfp.close()
139 143 if not diffs_seen:
140 144 os.unlink(tmpname)
141 145 return None, message, user, date, branch, None, None, None
142 146 p1 = parents and parents.pop(0) or None
143 147 p2 = parents and parents.pop(0) or None
144 148 return tmpname, message, user, date, branch, nodeid, p1, p2
145 149
146 150 GP_PATCH = 1 << 0 # we have to run patch
147 151 GP_FILTER = 1 << 1 # there's some copy/rename operation
148 152 GP_BINARY = 1 << 2 # there's a binary patch
149 153
150 154 class patchmeta(object):
151 155 """Patched file metadata
152 156
153 157 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
154 158 or COPY. 'path' is patched file path. 'oldpath' is set to the
155 159 origin file when 'op' is either COPY or RENAME, None otherwise. If
156 160 file mode is changed, 'mode' is a tuple (islink, isexec) where
157 161 'islink' is True if the file is a symlink and 'isexec' is True if
158 162 the file is executable. Otherwise, 'mode' is None.
159 163 """
160 164 def __init__(self, path):
161 165 self.path = path
162 166 self.oldpath = None
163 167 self.mode = None
164 168 self.op = 'MODIFY'
165 169 self.lineno = 0
166 170 self.binary = False
167 171
168 172 def setmode(self, mode):
169 173 islink = mode & 020000
170 174 isexec = mode & 0100
171 175 self.mode = (islink, isexec)
172 176
173 177 def readgitpatch(lr):
174 178 """extract git-style metadata about patches from <patchname>"""
175 179
176 180 # Filter patch for git information
177 181 gp = None
178 182 gitpatches = []
179 183 # Can have a git patch with only metadata, causing patch to complain
180 184 dopatch = 0
181 185
182 186 lineno = 0
183 187 for line in lr:
184 188 lineno += 1
185 189 line = line.rstrip(' \r\n')
186 190 if line.startswith('diff --git'):
187 191 m = gitre.match(line)
188 192 if m:
189 193 if gp:
190 194 gitpatches.append(gp)
191 195 dst = m.group(2)
192 196 gp = patchmeta(dst)
193 197 gp.lineno = lineno
194 198 elif gp:
195 199 if line.startswith('--- '):
196 200 if gp.op in ('COPY', 'RENAME'):
197 201 dopatch |= GP_FILTER
198 202 gitpatches.append(gp)
199 203 gp = None
200 204 dopatch |= GP_PATCH
201 205 continue
202 206 if line.startswith('rename from '):
203 207 gp.op = 'RENAME'
204 208 gp.oldpath = line[12:]
205 209 elif line.startswith('rename to '):
206 210 gp.path = line[10:]
207 211 elif line.startswith('copy from '):
208 212 gp.op = 'COPY'
209 213 gp.oldpath = line[10:]
210 214 elif line.startswith('copy to '):
211 215 gp.path = line[8:]
212 216 elif line.startswith('deleted file'):
213 217 gp.op = 'DELETE'
214 218 # is the deleted file a symlink?
215 219 gp.setmode(int(line[-6:], 8))
216 220 elif line.startswith('new file mode '):
217 221 gp.op = 'ADD'
218 222 gp.setmode(int(line[-6:], 8))
219 223 elif line.startswith('new mode '):
220 224 gp.setmode(int(line[-6:], 8))
221 225 elif line.startswith('GIT binary patch'):
222 226 dopatch |= GP_BINARY
223 227 gp.binary = True
224 228 if gp:
225 229 gitpatches.append(gp)
226 230
227 231 if not gitpatches:
228 232 dopatch = GP_PATCH
229 233
230 234 return (dopatch, gitpatches)
231 235
232 236 class linereader(object):
233 237 # simple class to allow pushing lines back into the input stream
234 238 def __init__(self, fp, textmode=False):
235 239 self.fp = fp
236 240 self.buf = []
237 241 self.textmode = textmode
238 242
239 243 def push(self, line):
240 244 if line is not None:
241 245 self.buf.append(line)
242 246
243 247 def readline(self):
244 248 if self.buf:
245 249 l = self.buf[0]
246 250 del self.buf[0]
247 251 return l
248 252 l = self.fp.readline()
249 253 if self.textmode and l.endswith('\r\n'):
250 254 l = l[:-2] + '\n'
251 255 return l
252 256
253 257 def __iter__(self):
254 258 while 1:
255 259 l = self.readline()
256 260 if not l:
257 261 break
258 262 yield l
259 263
260 264 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
261 265 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
262 266 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
263 267
264 268 class patchfile(object):
265 269 def __init__(self, ui, fname, opener, missing=False, eol=None):
266 270 self.fname = fname
267 271 self.eol = eol
268 272 self.opener = opener
269 273 self.ui = ui
270 274 self.lines = []
271 275 self.exists = False
272 276 self.missing = missing
273 277 if not missing:
274 278 try:
275 279 self.lines = self.readlines(fname)
276 280 self.exists = True
277 281 except IOError:
278 282 pass
279 283 else:
280 284 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
281 285
282 286 self.hash = {}
283 287 self.dirty = 0
284 288 self.offset = 0
285 289 self.rej = []
286 290 self.fileprinted = False
287 291 self.printfile(False)
288 292 self.hunks = 0
289 293
290 294 def readlines(self, fname):
291 295 fp = self.opener(fname, 'r')
292 296 try:
293 297 return list(linereader(fp, self.eol is not None))
294 298 finally:
295 299 fp.close()
296 300
297 301 def writelines(self, fname, lines):
298 302 fp = self.opener(fname, 'w')
299 303 try:
300 304 if self.eol and self.eol != '\n':
301 305 for l in lines:
302 306 if l and l[-1] == '\n':
303 307 l = l[:-1] + self.eol
304 308 fp.write(l)
305 309 else:
306 310 fp.writelines(lines)
307 311 finally:
308 312 fp.close()
309 313
310 314 def unlink(self, fname):
311 315 os.unlink(fname)
312 316
313 317 def printfile(self, warn):
314 318 if self.fileprinted:
315 319 return
316 320 if warn or self.ui.verbose:
317 321 self.fileprinted = True
318 322 s = _("patching file %s\n") % self.fname
319 323 if warn:
320 324 self.ui.warn(s)
321 325 else:
322 326 self.ui.note(s)
323 327
324 328
325 329 def findlines(self, l, linenum):
326 330 # looks through the hash and finds candidate lines. The
327 331 # result is a list of line numbers sorted based on distance
328 332 # from linenum
329 333
330 334 try:
331 335 cand = self.hash[l]
332 336 except:
333 337 return []
334 338
335 339 if len(cand) > 1:
336 340 # resort our list of potentials forward then back.
337 341 cand.sort(key=lambda x: abs(x - linenum))
338 342 return cand
339 343
340 344 def hashlines(self):
341 345 self.hash = {}
342 346 for x, s in enumerate(self.lines):
343 347 self.hash.setdefault(s, []).append(x)
344 348
345 349 def write_rej(self):
346 350 # our rejects are a little different from patch(1). This always
347 351 # creates rejects in the same form as the original patch. A file
348 352 # header is inserted so that you can run the reject through patch again
349 353 # without having to type the filename.
350 354
351 355 if not self.rej:
352 356 return
353 357
354 358 fname = self.fname + ".rej"
355 359 self.ui.warn(
356 360 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
357 361 (len(self.rej), self.hunks, fname))
358 362
359 363 def rejlines():
360 364 base = os.path.basename(self.fname)
361 365 yield "--- %s\n+++ %s\n" % (base, base)
362 366 for x in self.rej:
363 367 for l in x.hunk:
364 368 yield l
365 369 if l[-1] != '\n':
366 370 yield "\n\ No newline at end of file\n"
367 371
368 372 self.writelines(fname, rejlines())
369 373
370 374 def write(self, dest=None):
371 375 if not self.dirty:
372 376 return
373 377 if not dest:
374 378 dest = self.fname
375 379 self.writelines(dest, self.lines)
376 380
377 381 def close(self):
378 382 self.write()
379 383 self.write_rej()
380 384
381 385 def apply(self, h):
382 386 if not h.complete():
383 387 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
384 388 (h.number, h.desc, len(h.a), h.lena, len(h.b),
385 389 h.lenb))
386 390
387 391 self.hunks += 1
388 392
389 393 if self.missing:
390 394 self.rej.append(h)
391 395 return -1
392 396
393 397 if self.exists and h.createfile():
394 398 self.ui.warn(_("file %s already exists\n") % self.fname)
395 399 self.rej.append(h)
396 400 return -1
397 401
398 402 if isinstance(h, githunk):
399 403 if h.rmfile():
400 404 self.unlink(self.fname)
401 405 else:
402 406 self.lines[:] = h.new()
403 407 self.offset += len(h.new())
404 408 self.dirty = 1
405 409 return 0
406 410
407 411 # fast case first, no offsets, no fuzz
408 412 old = h.old()
409 413 # patch starts counting at 1 unless we are adding the file
410 414 if h.starta == 0:
411 415 start = 0
412 416 else:
413 417 start = h.starta + self.offset - 1
414 418 orig_start = start
415 419 if diffhelpers.testhunk(old, self.lines, start) == 0:
416 420 if h.rmfile():
417 421 self.unlink(self.fname)
418 422 else:
419 423 self.lines[start : start + h.lena] = h.new()
420 424 self.offset += h.lenb - h.lena
421 425 self.dirty = 1
422 426 return 0
423 427
424 428 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
425 429 self.hashlines()
426 430 if h.hunk[-1][0] != ' ':
427 431 # if the hunk tried to put something at the bottom of the file
428 432 # override the start line and use eof here
429 433 search_start = len(self.lines)
430 434 else:
431 435 search_start = orig_start
432 436
433 437 for fuzzlen in xrange(3):
434 438 for toponly in [ True, False ]:
435 439 old = h.old(fuzzlen, toponly)
436 440
437 441 cand = self.findlines(old[0][1:], search_start)
438 442 for l in cand:
439 443 if diffhelpers.testhunk(old, self.lines, l) == 0:
440 444 newlines = h.new(fuzzlen, toponly)
441 445 self.lines[l : l + len(old)] = newlines
442 446 self.offset += len(newlines) - len(old)
443 447 self.dirty = 1
444 448 if fuzzlen:
445 449 fuzzstr = "with fuzz %d " % fuzzlen
446 450 f = self.ui.warn
447 451 self.printfile(True)
448 452 else:
449 453 fuzzstr = ""
450 454 f = self.ui.note
451 455 offset = l - orig_start - fuzzlen
452 456 if offset == 1:
453 457 msg = _("Hunk #%d succeeded at %d %s"
454 458 "(offset %d line).\n")
455 459 else:
456 460 msg = _("Hunk #%d succeeded at %d %s"
457 461 "(offset %d lines).\n")
458 462 f(msg % (h.number, l+1, fuzzstr, offset))
459 463 return fuzzlen
460 464 self.printfile(True)
461 465 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
462 466 self.rej.append(h)
463 467 return -1
464 468
465 469 class hunk(object):
466 470 def __init__(self, desc, num, lr, context, create=False, remove=False):
467 471 self.number = num
468 472 self.desc = desc
469 473 self.hunk = [ desc ]
470 474 self.a = []
471 475 self.b = []
472 476 if context:
473 477 self.read_context_hunk(lr)
474 478 else:
475 479 self.read_unified_hunk(lr)
476 480 self.create = create
477 481 self.remove = remove and not create
478 482
479 483 def read_unified_hunk(self, lr):
480 484 m = unidesc.match(self.desc)
481 485 if not m:
482 486 raise PatchError(_("bad hunk #%d") % self.number)
483 487 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
484 488 if self.lena is None:
485 489 self.lena = 1
486 490 else:
487 491 self.lena = int(self.lena)
488 492 if self.lenb is None:
489 493 self.lenb = 1
490 494 else:
491 495 self.lenb = int(self.lenb)
492 496 self.starta = int(self.starta)
493 497 self.startb = int(self.startb)
494 498 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
495 499 # if we hit eof before finishing out the hunk, the last line will
496 500 # be zero length. Lets try to fix it up.
497 501 while len(self.hunk[-1]) == 0:
498 502 del self.hunk[-1]
499 503 del self.a[-1]
500 504 del self.b[-1]
501 505 self.lena -= 1
502 506 self.lenb -= 1
503 507
504 508 def read_context_hunk(self, lr):
505 509 self.desc = lr.readline()
506 510 m = contextdesc.match(self.desc)
507 511 if not m:
508 512 raise PatchError(_("bad hunk #%d") % self.number)
509 513 foo, self.starta, foo2, aend, foo3 = m.groups()
510 514 self.starta = int(self.starta)
511 515 if aend is None:
512 516 aend = self.starta
513 517 self.lena = int(aend) - self.starta
514 518 if self.starta:
515 519 self.lena += 1
516 520 for x in xrange(self.lena):
517 521 l = lr.readline()
518 522 if l.startswith('---'):
519 523 lr.push(l)
520 524 break
521 525 s = l[2:]
522 526 if l.startswith('- ') or l.startswith('! '):
523 527 u = '-' + s
524 528 elif l.startswith(' '):
525 529 u = ' ' + s
526 530 else:
527 531 raise PatchError(_("bad hunk #%d old text line %d") %
528 532 (self.number, x))
529 533 self.a.append(u)
530 534 self.hunk.append(u)
531 535
532 536 l = lr.readline()
533 537 if l.startswith('\ '):
534 538 s = self.a[-1][:-1]
535 539 self.a[-1] = s
536 540 self.hunk[-1] = s
537 541 l = lr.readline()
538 542 m = contextdesc.match(l)
539 543 if not m:
540 544 raise PatchError(_("bad hunk #%d") % self.number)
541 545 foo, self.startb, foo2, bend, foo3 = m.groups()
542 546 self.startb = int(self.startb)
543 547 if bend is None:
544 548 bend = self.startb
545 549 self.lenb = int(bend) - self.startb
546 550 if self.startb:
547 551 self.lenb += 1
548 552 hunki = 1
549 553 for x in xrange(self.lenb):
550 554 l = lr.readline()
551 555 if l.startswith('\ '):
552 556 s = self.b[-1][:-1]
553 557 self.b[-1] = s
554 558 self.hunk[hunki-1] = s
555 559 continue
556 560 if not l:
557 561 lr.push(l)
558 562 break
559 563 s = l[2:]
560 564 if l.startswith('+ ') or l.startswith('! '):
561 565 u = '+' + s
562 566 elif l.startswith(' '):
563 567 u = ' ' + s
564 568 elif len(self.b) == 0:
565 569 # this can happen when the hunk does not add any lines
566 570 lr.push(l)
567 571 break
568 572 else:
569 573 raise PatchError(_("bad hunk #%d old text line %d") %
570 574 (self.number, x))
571 575 self.b.append(s)
572 576 while True:
573 577 if hunki >= len(self.hunk):
574 578 h = ""
575 579 else:
576 580 h = self.hunk[hunki]
577 581 hunki += 1
578 582 if h == u:
579 583 break
580 584 elif h.startswith('-'):
581 585 continue
582 586 else:
583 587 self.hunk.insert(hunki-1, u)
584 588 break
585 589
586 590 if not self.a:
587 591 # this happens when lines were only added to the hunk
588 592 for x in self.hunk:
589 593 if x.startswith('-') or x.startswith(' '):
590 594 self.a.append(x)
591 595 if not self.b:
592 596 # this happens when lines were only deleted from the hunk
593 597 for x in self.hunk:
594 598 if x.startswith('+') or x.startswith(' '):
595 599 self.b.append(x[1:])
596 600 # @@ -start,len +start,len @@
597 601 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
598 602 self.startb, self.lenb)
599 603 self.hunk[0] = self.desc
600 604
601 605 def fix_newline(self):
602 606 diffhelpers.fix_newline(self.hunk, self.a, self.b)
603 607
604 608 def complete(self):
605 609 return len(self.a) == self.lena and len(self.b) == self.lenb
606 610
607 611 def createfile(self):
608 612 return self.starta == 0 and self.lena == 0 and self.create
609 613
610 614 def rmfile(self):
611 615 return self.startb == 0 and self.lenb == 0 and self.remove
612 616
613 617 def fuzzit(self, l, fuzz, toponly):
614 618 # this removes context lines from the top and bottom of list 'l'. It
615 619 # checks the hunk to make sure only context lines are removed, and then
616 620 # returns a new shortened list of lines.
617 621 fuzz = min(fuzz, len(l)-1)
618 622 if fuzz:
619 623 top = 0
620 624 bot = 0
621 625 hlen = len(self.hunk)
622 626 for x in xrange(hlen-1):
623 627 # the hunk starts with the @@ line, so use x+1
624 628 if self.hunk[x+1][0] == ' ':
625 629 top += 1
626 630 else:
627 631 break
628 632 if not toponly:
629 633 for x in xrange(hlen-1):
630 634 if self.hunk[hlen-bot-1][0] == ' ':
631 635 bot += 1
632 636 else:
633 637 break
634 638
635 639 # top and bot now count context in the hunk
636 640 # adjust them if either one is short
637 641 context = max(top, bot, 3)
638 642 if bot < context:
639 643 bot = max(0, fuzz - (context - bot))
640 644 else:
641 645 bot = min(fuzz, bot)
642 646 if top < context:
643 647 top = max(0, fuzz - (context - top))
644 648 else:
645 649 top = min(fuzz, top)
646 650
647 651 return l[top:len(l)-bot]
648 652 return l
649 653
650 654 def old(self, fuzz=0, toponly=False):
651 655 return self.fuzzit(self.a, fuzz, toponly)
652 656
653 657 def newctrl(self):
654 658 res = []
655 659 for x in self.hunk:
656 660 c = x[0]
657 661 if c == ' ' or c == '+':
658 662 res.append(x)
659 663 return res
660 664
661 665 def new(self, fuzz=0, toponly=False):
662 666 return self.fuzzit(self.b, fuzz, toponly)
663 667
664 668 class githunk(object):
665 669 """A git hunk"""
666 670 def __init__(self, gitpatch):
667 671 self.gitpatch = gitpatch
668 672 self.text = None
669 673 self.hunk = []
670 674
671 675 def createfile(self):
672 676 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
673 677
674 678 def rmfile(self):
675 679 return self.gitpatch.op == 'DELETE'
676 680
677 681 def complete(self):
678 682 return self.text is not None
679 683
680 684 def new(self):
681 685 return [self.text]
682 686
683 687 class binhunk(githunk):
684 688 'A binary patch file. Only understands literals so far.'
685 689 def __init__(self, gitpatch):
686 690 super(binhunk, self).__init__(gitpatch)
687 691 self.hunk = ['GIT binary patch\n']
688 692
689 693 def extract(self, lr):
690 694 line = lr.readline()
691 695 self.hunk.append(line)
692 696 while line and not line.startswith('literal '):
693 697 line = lr.readline()
694 698 self.hunk.append(line)
695 699 if not line:
696 700 raise PatchError(_('could not extract binary patch'))
697 701 size = int(line[8:].rstrip())
698 702 dec = []
699 703 line = lr.readline()
700 704 self.hunk.append(line)
701 705 while len(line) > 1:
702 706 l = line[0]
703 707 if l <= 'Z' and l >= 'A':
704 708 l = ord(l) - ord('A') + 1
705 709 else:
706 710 l = ord(l) - ord('a') + 27
707 711 dec.append(base85.b85decode(line[1:-1])[:l])
708 712 line = lr.readline()
709 713 self.hunk.append(line)
710 714 text = zlib.decompress(''.join(dec))
711 715 if len(text) != size:
712 716 raise PatchError(_('binary patch is %d bytes, not %d') %
713 717 len(text), size)
714 718 self.text = text
715 719
716 720 class symlinkhunk(githunk):
717 721 """A git symlink hunk"""
718 722 def __init__(self, gitpatch, hunk):
719 723 super(symlinkhunk, self).__init__(gitpatch)
720 724 self.hunk = hunk
721 725
722 726 def complete(self):
723 727 return True
724 728
725 729 def fix_newline(self):
726 730 return
727 731
728 732 def parsefilename(str):
729 733 # --- filename \t|space stuff
730 734 s = str[4:].rstrip('\r\n')
731 735 i = s.find('\t')
732 736 if i < 0:
733 737 i = s.find(' ')
734 738 if i < 0:
735 739 return s
736 740 return s[:i]
737 741
738 742 def selectfile(afile_orig, bfile_orig, hunk, strip):
739 743 def pathstrip(path, count=1):
740 744 pathlen = len(path)
741 745 i = 0
742 746 if count == 0:
743 747 return '', path.rstrip()
744 748 while count > 0:
745 749 i = path.find('/', i)
746 750 if i == -1:
747 751 raise PatchError(_("unable to strip away %d dirs from %s") %
748 752 (count, path))
749 753 i += 1
750 754 # consume '//' in the path
751 755 while i < pathlen - 1 and path[i] == '/':
752 756 i += 1
753 757 count -= 1
754 758 return path[:i].lstrip(), path[i:].rstrip()
755 759
756 760 nulla = afile_orig == "/dev/null"
757 761 nullb = bfile_orig == "/dev/null"
758 762 abase, afile = pathstrip(afile_orig, strip)
759 763 gooda = not nulla and util.lexists(afile)
760 764 bbase, bfile = pathstrip(bfile_orig, strip)
761 765 if afile == bfile:
762 766 goodb = gooda
763 767 else:
764 768 goodb = not nullb and os.path.exists(bfile)
765 769 createfunc = hunk.createfile
766 770 missing = not goodb and not gooda and not createfunc()
767 771
768 772 # some diff programs apparently produce create patches where the
769 773 # afile is not /dev/null, but rather the same name as the bfile
770 774 if missing and afile == bfile:
771 775 # this isn't very pretty
772 776 hunk.create = True
773 777 if createfunc():
774 778 missing = False
775 779 else:
776 780 hunk.create = False
777 781
778 782 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
779 783 # diff is between a file and its backup. In this case, the original
780 784 # file should be patched (see original mpatch code).
781 785 isbackup = (abase == bbase and bfile.startswith(afile))
782 786 fname = None
783 787 if not missing:
784 788 if gooda and goodb:
785 789 fname = isbackup and afile or bfile
786 790 elif gooda:
787 791 fname = afile
788 792
789 793 if not fname:
790 794 if not nullb:
791 795 fname = isbackup and afile or bfile
792 796 elif not nulla:
793 797 fname = afile
794 798 else:
795 799 raise PatchError(_("undefined source and destination files"))
796 800
797 801 return fname, missing
798 802
799 803 def scangitpatch(lr, firstline):
800 804 """
801 805 Git patches can emit:
802 806 - rename a to b
803 807 - change b
804 808 - copy a to c
805 809 - change c
806 810
807 811 We cannot apply this sequence as-is, the renamed 'a' could not be
808 812 found for it would have been renamed already. And we cannot copy
809 813 from 'b' instead because 'b' would have been changed already. So
810 814 we scan the git patch for copy and rename commands so we can
811 815 perform the copies ahead of time.
812 816 """
813 817 pos = 0
814 818 try:
815 819 pos = lr.fp.tell()
816 820 fp = lr.fp
817 821 except IOError:
818 822 fp = cStringIO.StringIO(lr.fp.read())
819 823 gitlr = linereader(fp, lr.textmode)
820 824 gitlr.push(firstline)
821 825 (dopatch, gitpatches) = readgitpatch(gitlr)
822 826 fp.seek(pos)
823 827 return dopatch, gitpatches
824 828
825 829 def iterhunks(ui, fp, sourcefile=None, textmode=False):
826 830 """Read a patch and yield the following events:
827 831 - ("file", afile, bfile, firsthunk): select a new target file.
828 832 - ("hunk", hunk): a new hunk is ready to be applied, follows a
829 833 "file" event.
830 834 - ("git", gitchanges): current diff is in git format, gitchanges
831 835 maps filenames to gitpatch records. Unique event.
832 836
833 837 If textmode is True, input line-endings are normalized to LF.
834 838 """
835 839 changed = {}
836 840 current_hunk = None
837 841 afile = ""
838 842 bfile = ""
839 843 state = None
840 844 hunknum = 0
841 845 emitfile = False
842 846 git = False
843 847
844 848 # our states
845 849 BFILE = 1
846 850 context = None
847 851 lr = linereader(fp, textmode)
848 852 dopatch = True
849 853 # gitworkdone is True if a git operation (copy, rename, ...) was
850 854 # performed already for the current file. Useful when the file
851 855 # section may have no hunk.
852 856 gitworkdone = False
853 857
854 858 while True:
855 859 newfile = False
856 860 x = lr.readline()
857 861 if not x:
858 862 break
859 863 if current_hunk:
860 864 if x.startswith('\ '):
861 865 current_hunk.fix_newline()
862 866 yield 'hunk', current_hunk
863 867 current_hunk = None
864 868 gitworkdone = False
865 869 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
866 870 ((context is not False) and x.startswith('***************')))):
867 871 try:
868 872 if context is None and x.startswith('***************'):
869 873 context = True
870 874 gpatch = changed.get(bfile)
871 875 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
872 876 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
873 877 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
874 878 if remove:
875 879 gpatch = changed.get(afile[2:])
876 880 if gpatch and gpatch.mode[0]:
877 881 current_hunk = symlinkhunk(gpatch, current_hunk)
878 882 except PatchError, err:
879 883 ui.debug(err)
880 884 current_hunk = None
881 885 continue
882 886 hunknum += 1
883 887 if emitfile:
884 888 emitfile = False
885 889 yield 'file', (afile, bfile, current_hunk)
886 890 elif state == BFILE and x.startswith('GIT binary patch'):
887 891 current_hunk = binhunk(changed[bfile])
888 892 hunknum += 1
889 893 if emitfile:
890 894 emitfile = False
891 895 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
892 896 current_hunk.extract(lr)
893 897 elif x.startswith('diff --git'):
894 898 # check for git diff, scanning the whole patch file if needed
895 899 m = gitre.match(x)
896 900 if m:
897 901 afile, bfile = m.group(1, 2)
898 902 if not git:
899 903 git = True
900 904 dopatch, gitpatches = scangitpatch(lr, x)
901 905 yield 'git', gitpatches
902 906 for gp in gitpatches:
903 907 changed[gp.path] = gp
904 908 # else error?
905 909 # copy/rename + modify should modify target, not source
906 910 gp = changed.get(bfile)
907 911 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
908 912 afile = bfile
909 913 gitworkdone = True
910 914 newfile = True
911 915 elif x.startswith('---'):
912 916 # check for a unified diff
913 917 l2 = lr.readline()
914 918 if not l2.startswith('+++'):
915 919 lr.push(l2)
916 920 continue
917 921 newfile = True
918 922 context = False
919 923 afile = parsefilename(x)
920 924 bfile = parsefilename(l2)
921 925 elif x.startswith('***'):
922 926 # check for a context diff
923 927 l2 = lr.readline()
924 928 if not l2.startswith('---'):
925 929 lr.push(l2)
926 930 continue
927 931 l3 = lr.readline()
928 932 lr.push(l3)
929 933 if not l3.startswith("***************"):
930 934 lr.push(l2)
931 935 continue
932 936 newfile = True
933 937 context = True
934 938 afile = parsefilename(x)
935 939 bfile = parsefilename(l2)
936 940
937 941 if newfile:
938 942 emitfile = True
939 943 state = BFILE
940 944 hunknum = 0
941 945 if current_hunk:
942 946 if current_hunk.complete():
943 947 yield 'hunk', current_hunk
944 948 else:
945 949 raise PatchError(_("malformed patch %s %s") % (afile,
946 950 current_hunk.desc))
947 951
948 952 if hunknum == 0 and dopatch and not gitworkdone:
949 953 raise NoHunks
950 954
951 955 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eol=None):
952 956 """
953 957 Reads a patch from fp and tries to apply it.
954 958
955 959 The dict 'changed' is filled in with all of the filenames changed
956 960 by the patch. Returns 0 for a clean patch, -1 if any rejects were
957 961 found and 1 if there was any fuzz.
958 962
959 963 If 'eol' is None, the patch content and patched file are read in
960 964 binary mode. Otherwise, line endings are ignored when patching then
961 965 normalized to 'eol' (usually '\n' or \r\n').
962 966 """
963 967 rejects = 0
964 968 err = 0
965 969 current_file = None
966 970 gitpatches = None
967 971 opener = util.opener(os.getcwd())
968 972 textmode = eol is not None
969 973
970 974 def closefile():
971 975 if not current_file:
972 976 return 0
973 977 current_file.close()
974 978 return len(current_file.rej)
975 979
976 980 for state, values in iterhunks(ui, fp, sourcefile, textmode):
977 981 if state == 'hunk':
978 982 if not current_file:
979 983 continue
980 984 current_hunk = values
981 985 ret = current_file.apply(current_hunk)
982 986 if ret >= 0:
983 987 changed.setdefault(current_file.fname, None)
984 988 if ret > 0:
985 989 err = 1
986 990 elif state == 'file':
987 991 rejects += closefile()
988 992 afile, bfile, first_hunk = values
989 993 try:
990 994 if sourcefile:
991 995 current_file = patchfile(ui, sourcefile, opener, eol=eol)
992 996 else:
993 997 current_file, missing = selectfile(afile, bfile, first_hunk,
994 998 strip)
995 999 current_file = patchfile(ui, current_file, opener, missing, eol)
996 1000 except PatchError, err:
997 1001 ui.warn(str(err) + '\n')
998 1002 current_file, current_hunk = None, None
999 1003 rejects += 1
1000 1004 continue
1001 1005 elif state == 'git':
1002 1006 gitpatches = values
1003 1007 cwd = os.getcwd()
1004 1008 for gp in gitpatches:
1005 1009 if gp.op in ('COPY', 'RENAME'):
1006 1010 copyfile(gp.oldpath, gp.path, cwd)
1007 1011 changed[gp.path] = gp
1008 1012 else:
1009 1013 raise util.Abort(_('unsupported parser state: %s') % state)
1010 1014
1011 1015 rejects += closefile()
1012 1016
1013 1017 if rejects:
1014 1018 return -1
1015 1019 return err
1016 1020
1017 1021 def diffopts(ui, opts={}, untrusted=False):
1018 1022 def get(key, name=None, getter=ui.configbool):
1019 1023 return (opts.get(key) or
1020 1024 getter('diff', name or key, None, untrusted=untrusted))
1021 1025 return mdiff.diffopts(
1022 1026 text=opts.get('text'),
1023 1027 git=get('git'),
1024 1028 nodates=get('nodates'),
1025 1029 showfunc=get('show_function', 'showfunc'),
1026 1030 ignorews=get('ignore_all_space', 'ignorews'),
1027 1031 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1028 1032 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1029 1033 context=get('unified', getter=ui.config))
1030 1034
1031 1035 def updatedir(ui, repo, patches, similarity=0):
1032 1036 '''Update dirstate after patch application according to metadata'''
1033 1037 if not patches:
1034 1038 return
1035 1039 copies = []
1036 1040 removes = set()
1037 1041 cfiles = patches.keys()
1038 1042 cwd = repo.getcwd()
1039 1043 if cwd:
1040 1044 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1041 1045 for f in patches:
1042 1046 gp = patches[f]
1043 1047 if not gp:
1044 1048 continue
1045 1049 if gp.op == 'RENAME':
1046 1050 copies.append((gp.oldpath, gp.path))
1047 1051 removes.add(gp.oldpath)
1048 1052 elif gp.op == 'COPY':
1049 1053 copies.append((gp.oldpath, gp.path))
1050 1054 elif gp.op == 'DELETE':
1051 1055 removes.add(gp.path)
1052 1056 for src, dst in copies:
1053 1057 repo.copy(src, dst)
1054 1058 if (not similarity) and removes:
1055 1059 repo.remove(sorted(removes), True)
1056 1060 for f in patches:
1057 1061 gp = patches[f]
1058 1062 if gp and gp.mode:
1059 1063 islink, isexec = gp.mode
1060 1064 dst = repo.wjoin(gp.path)
1061 1065 # patch won't create empty files
1062 1066 if gp.op == 'ADD' and not os.path.exists(dst):
1063 1067 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1064 1068 repo.wwrite(gp.path, '', flags)
1065 1069 elif gp.op != 'DELETE':
1066 1070 util.set_flags(dst, islink, isexec)
1067 1071 cmdutil.addremove(repo, cfiles, similarity=similarity)
1068 1072 files = patches.keys()
1069 1073 files.extend([r for r in removes if r not in files])
1070 1074 return sorted(files)
1071 1075
1072 1076 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1073 1077 """use <patcher> to apply <patchname> to the working directory.
1074 1078 returns whether patch was applied with fuzz factor."""
1075 1079
1076 1080 fuzz = False
1077 1081 if cwd:
1078 1082 args.append('-d %s' % util.shellquote(cwd))
1079 1083 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1080 1084 util.shellquote(patchname)))
1081 1085
1082 1086 for line in fp:
1083 1087 line = line.rstrip()
1084 1088 ui.note(line + '\n')
1085 1089 if line.startswith('patching file '):
1086 1090 pf = util.parse_patch_output(line)
1087 1091 printed_file = False
1088 1092 files.setdefault(pf, None)
1089 1093 elif line.find('with fuzz') >= 0:
1090 1094 fuzz = True
1091 1095 if not printed_file:
1092 1096 ui.warn(pf + '\n')
1093 1097 printed_file = True
1094 1098 ui.warn(line + '\n')
1095 1099 elif line.find('saving rejects to file') >= 0:
1096 1100 ui.warn(line + '\n')
1097 1101 elif line.find('FAILED') >= 0:
1098 1102 if not printed_file:
1099 1103 ui.warn(pf + '\n')
1100 1104 printed_file = True
1101 1105 ui.warn(line + '\n')
1102 1106 code = fp.close()
1103 1107 if code:
1104 1108 raise PatchError(_("patch command failed: %s") %
1105 1109 util.explain_exit(code)[0])
1106 1110 return fuzz
1107 1111
1108 1112 def internalpatch(patchobj, ui, strip, cwd, files={}, eolmode='strict'):
1109 1113 """use builtin patch to apply <patchobj> to the working directory.
1110 1114 returns whether patch was applied with fuzz factor."""
1111 1115
1112 1116 if eolmode is None:
1113 1117 eolmode = ui.config('patch', 'eol', 'strict')
1114 1118 try:
1115 1119 eol = {'strict': None, 'crlf': '\r\n', 'lf': '\n'}[eolmode.lower()]
1116 1120 except KeyError:
1117 1121 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1118 1122
1119 1123 try:
1120 1124 fp = open(patchobj, 'rb')
1121 1125 except TypeError:
1122 1126 fp = patchobj
1123 1127 if cwd:
1124 1128 curdir = os.getcwd()
1125 1129 os.chdir(cwd)
1126 1130 try:
1127 1131 ret = applydiff(ui, fp, files, strip=strip, eol=eol)
1128 1132 finally:
1129 1133 if cwd:
1130 1134 os.chdir(curdir)
1131 1135 if ret < 0:
1132 1136 raise PatchError
1133 1137 return ret > 0
1134 1138
1135 1139 def patch(patchname, ui, strip=1, cwd=None, files={}, eolmode='strict'):
1136 1140 """Apply <patchname> to the working directory.
1137 1141
1138 1142 'eolmode' specifies how end of lines should be handled. It can be:
1139 1143 - 'strict': inputs are read in binary mode, EOLs are preserved
1140 1144 - 'crlf': EOLs are ignored when patching and reset to CRLF
1141 1145 - 'lf': EOLs are ignored when patching and reset to LF
1142 1146 - None: get it from user settings, default to 'strict'
1143 1147 'eolmode' is ignored when using an external patcher program.
1144 1148
1145 1149 Returns whether patch was applied with fuzz factor.
1146 1150 """
1147 1151 patcher = ui.config('ui', 'patch')
1148 1152 args = []
1149 1153 try:
1150 1154 if patcher:
1151 1155 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1152 1156 files)
1153 1157 else:
1154 1158 try:
1155 1159 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1156 1160 except NoHunks:
1157 1161 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch'
1158 1162 ui.debug('no valid hunks found; trying with %r instead\n' %
1159 1163 patcher)
1160 1164 if util.needbinarypatch():
1161 1165 args.append('--binary')
1162 1166 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1163 1167 files)
1164 1168 except PatchError, err:
1165 1169 s = str(err)
1166 1170 if s:
1167 1171 raise util.Abort(s)
1168 1172 else:
1169 1173 raise util.Abort(_('patch failed to apply'))
1170 1174
1171 1175 def b85diff(to, tn):
1172 1176 '''print base85-encoded binary diff'''
1173 1177 def gitindex(text):
1174 1178 if not text:
1175 1179 return '0' * 40
1176 1180 l = len(text)
1177 1181 s = util.sha1('blob %d\0' % l)
1178 1182 s.update(text)
1179 1183 return s.hexdigest()
1180 1184
1181 1185 def fmtline(line):
1182 1186 l = len(line)
1183 1187 if l <= 26:
1184 1188 l = chr(ord('A') + l - 1)
1185 1189 else:
1186 1190 l = chr(l - 26 + ord('a') - 1)
1187 1191 return '%c%s\n' % (l, base85.b85encode(line, True))
1188 1192
1189 1193 def chunk(text, csize=52):
1190 1194 l = len(text)
1191 1195 i = 0
1192 1196 while i < l:
1193 1197 yield text[i:i+csize]
1194 1198 i += csize
1195 1199
1196 1200 tohash = gitindex(to)
1197 1201 tnhash = gitindex(tn)
1198 1202 if tohash == tnhash:
1199 1203 return ""
1200 1204
1201 1205 # TODO: deltas
1202 1206 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1203 1207 (tohash, tnhash, len(tn))]
1204 1208 for l in chunk(zlib.compress(tn)):
1205 1209 ret.append(fmtline(l))
1206 1210 ret.append('\n')
1207 1211 return ''.join(ret)
1208 1212
1209 1213 def _addmodehdr(header, omode, nmode):
1210 1214 if omode != nmode:
1211 1215 header.append('old mode %s\n' % omode)
1212 1216 header.append('new mode %s\n' % nmode)
1213 1217
1214 1218 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None):
1215 1219 '''yields diff of changes to files between two nodes, or node and
1216 1220 working directory.
1217 1221
1218 1222 if node1 is None, use first dirstate parent instead.
1219 1223 if node2 is None, compare node1 with working directory.'''
1220 1224
1221 1225 if opts is None:
1222 1226 opts = mdiff.defaultopts
1223 1227
1224 1228 if not node1:
1225 1229 node1 = repo.dirstate.parents()[0]
1226 1230
1227 1231 def lrugetfilectx():
1228 1232 cache = {}
1229 1233 order = []
1230 1234 def getfilectx(f, ctx):
1231 1235 fctx = ctx.filectx(f, filelog=cache.get(f))
1232 1236 if f not in cache:
1233 1237 if len(cache) > 20:
1234 1238 del cache[order.pop(0)]
1235 1239 cache[f] = fctx._filelog
1236 1240 else:
1237 1241 order.remove(f)
1238 1242 order.append(f)
1239 1243 return fctx
1240 1244 return getfilectx
1241 1245 getfilectx = lrugetfilectx()
1242 1246
1243 1247 ctx1 = repo[node1]
1244 1248 ctx2 = repo[node2]
1245 1249
1246 1250 if not changes:
1247 1251 changes = repo.status(ctx1, ctx2, match=match)
1248 1252 modified, added, removed = changes[:3]
1249 1253
1250 1254 if not modified and not added and not removed:
1251 1255 return
1252 1256
1253 1257 date1 = util.datestr(ctx1.date())
1254 1258 man1 = ctx1.manifest()
1255 1259
1256 1260 if repo.ui.quiet:
1257 1261 r = None
1258 1262 else:
1259 1263 hexfunc = repo.ui.debugflag and hex or short
1260 1264 r = [hexfunc(node) for node in [node1, node2] if node]
1261 1265
1262 1266 if opts.git:
1263 1267 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
1264 1268 copy = copy.copy()
1265 1269 for k, v in copy.items():
1266 1270 copy[v] = k
1267 1271
1268 1272 gone = set()
1269 1273 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1270 1274
1271 1275 for f in sorted(modified + added + removed):
1272 1276 to = None
1273 1277 tn = None
1274 1278 dodiff = True
1275 1279 header = []
1276 1280 if f in man1:
1277 1281 to = getfilectx(f, ctx1).data()
1278 1282 if f not in removed:
1279 1283 tn = getfilectx(f, ctx2).data()
1280 1284 a, b = f, f
1281 1285 if opts.git:
1282 1286 if f in added:
1283 1287 mode = gitmode[ctx2.flags(f)]
1284 1288 if f in copy:
1285 1289 a = copy[f]
1286 1290 omode = gitmode[man1.flags(a)]
1287 1291 _addmodehdr(header, omode, mode)
1288 1292 if a in removed and a not in gone:
1289 1293 op = 'rename'
1290 1294 gone.add(a)
1291 1295 else:
1292 1296 op = 'copy'
1293 1297 header.append('%s from %s\n' % (op, a))
1294 1298 header.append('%s to %s\n' % (op, f))
1295 1299 to = getfilectx(a, ctx1).data()
1296 1300 else:
1297 1301 header.append('new file mode %s\n' % mode)
1298 1302 if util.binary(tn):
1299 1303 dodiff = 'binary'
1300 1304 elif f in removed:
1301 1305 # have we already reported a copy above?
1302 1306 if f in copy and copy[f] in added and copy[copy[f]] == f:
1303 1307 dodiff = False
1304 1308 else:
1305 1309 header.append('deleted file mode %s\n' %
1306 1310 gitmode[man1.flags(f)])
1307 1311 else:
1308 1312 omode = gitmode[man1.flags(f)]
1309 1313 nmode = gitmode[ctx2.flags(f)]
1310 1314 _addmodehdr(header, omode, nmode)
1311 1315 if util.binary(to) or util.binary(tn):
1312 1316 dodiff = 'binary'
1313 1317 r = None
1314 1318 header.insert(0, mdiff.diffline(r, a, b, opts))
1315 1319 if dodiff:
1316 1320 if dodiff == 'binary':
1317 1321 text = b85diff(to, tn)
1318 1322 else:
1319 1323 text = mdiff.unidiff(to, date1,
1320 1324 # ctx2 date may be dynamic
1321 1325 tn, util.datestr(ctx2.date()),
1322 1326 a, b, r, opts=opts)
1323 1327 if header and (text or len(header) > 1):
1324 1328 yield ''.join(header)
1325 1329 if text:
1326 1330 yield text
1327 1331
1328 1332 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1329 1333 opts=None):
1330 1334 '''export changesets as hg patches.'''
1331 1335
1332 1336 total = len(revs)
1333 1337 revwidth = max([len(str(rev)) for rev in revs])
1334 1338
1335 1339 def single(rev, seqno, fp):
1336 1340 ctx = repo[rev]
1337 1341 node = ctx.node()
1338 1342 parents = [p.node() for p in ctx.parents() if p]
1339 1343 branch = ctx.branch()
1340 1344 if switch_parent:
1341 1345 parents.reverse()
1342 1346 prev = (parents and parents[0]) or nullid
1343 1347
1344 1348 if not fp:
1345 1349 fp = cmdutil.make_file(repo, template, node, total=total,
1346 1350 seqno=seqno, revwidth=revwidth,
1347 1351 mode='ab')
1348 1352 if fp != sys.stdout and hasattr(fp, 'name'):
1349 1353 repo.ui.note("%s\n" % fp.name)
1350 1354
1351 1355 fp.write("# HG changeset patch\n")
1352 1356 fp.write("# User %s\n" % ctx.user())
1353 1357 fp.write("# Date %d %d\n" % ctx.date())
1354 1358 if branch and (branch != 'default'):
1355 1359 fp.write("# Branch %s\n" % branch)
1356 1360 fp.write("# Node ID %s\n" % hex(node))
1357 1361 fp.write("# Parent %s\n" % hex(prev))
1358 1362 if len(parents) > 1:
1359 1363 fp.write("# Parent %s\n" % hex(parents[1]))
1360 1364 fp.write(ctx.description().rstrip())
1361 1365 fp.write("\n\n")
1362 1366
1363 1367 for chunk in diff(repo, prev, node, opts=opts):
1364 1368 fp.write(chunk)
1365 1369
1366 1370 for seqno, rev in enumerate(revs):
1367 1371 single(rev, seqno+1, fp)
1368 1372
1369 1373 def diffstatdata(lines):
1370 1374 filename, adds, removes = None, 0, 0
1371 1375 for line in lines:
1372 1376 if line.startswith('diff'):
1373 1377 if filename:
1374 1378 yield (filename, adds, removes)
1375 1379 # set numbers to 0 anyway when starting new file
1376 1380 adds, removes = 0, 0
1377 1381 if line.startswith('diff --git'):
1378 1382 filename = gitre.search(line).group(1)
1379 1383 else:
1380 1384 # format: "diff -r ... -r ... filename"
1381 1385 filename = line.split(None, 5)[-1]
1382 1386 elif line.startswith('+') and not line.startswith('+++'):
1383 1387 adds += 1
1384 1388 elif line.startswith('-') and not line.startswith('---'):
1385 1389 removes += 1
1386 1390 if filename:
1387 1391 yield (filename, adds, removes)
1388 1392
1389 1393 def diffstat(lines, width=80):
1390 1394 output = []
1391 1395 stats = list(diffstatdata(lines))
1392 1396
1393 1397 maxtotal, maxname = 0, 0
1394 1398 totaladds, totalremoves = 0, 0
1395 1399 for filename, adds, removes in stats:
1396 1400 totaladds += adds
1397 1401 totalremoves += removes
1398 1402 maxname = max(maxname, len(filename))
1399 1403 maxtotal = max(maxtotal, adds+removes)
1400 1404
1401 1405 countwidth = len(str(maxtotal))
1402 1406 graphwidth = width - countwidth - maxname - 6
1403 1407 if graphwidth < 10:
1404 1408 graphwidth = 10
1405 1409
1406 1410 def scale(i):
1407 1411 if maxtotal <= graphwidth:
1408 1412 return i
1409 1413 # If diffstat runs out of room it doesn't print anything,
1410 1414 # which isn't very useful, so always print at least one + or -
1411 1415 # if there were at least some changes.
1412 1416 return max(i * graphwidth // maxtotal, int(bool(i)))
1413 1417
1414 1418 for filename, adds, removes in stats:
1415 1419 pluses = '+' * scale(adds)
1416 1420 minuses = '-' * scale(removes)
1417 1421 output.append(' %-*s | %*.d %s%s\n' % (maxname, filename, countwidth,
1418 1422 adds+removes, pluses, minuses))
1419 1423
1420 1424 if stats:
1421 1425 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1422 1426 % (len(stats), totaladds, totalremoves))
1423 1427
1424 1428 return ''.join(output)
@@ -1,292 +1,292
1 1 # windows.py - Windows utility function implementations for Mercurial
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 8 from i18n import _
9 9 import osutil, error
10 10 import errno, msvcrt, os, re, sys
11 11
12 12 nulldev = 'NUL:'
13 13 umask = 002
14 14
15 15 # wrap osutil.posixfile to provide friendlier exceptions
16 16 def posixfile(name, mode='r', buffering=-1):
17 17 try:
18 18 return osutil.posixfile(name, mode, buffering)
19 19 except WindowsError, err:
20 20 raise IOError(err.errno, '%s: %s' % (name, err.strerror))
21 21 posixfile.__doc__ = osutil.posixfile.__doc__
22 22
23 23 class winstdout(object):
24 24 '''stdout on windows misbehaves if sent through a pipe'''
25 25
26 26 def __init__(self, fp):
27 27 self.fp = fp
28 28
29 29 def __getattr__(self, key):
30 30 return getattr(self.fp, key)
31 31
32 32 def close(self):
33 33 try:
34 34 self.fp.close()
35 35 except: pass
36 36
37 37 def write(self, s):
38 38 try:
39 39 # This is workaround for "Not enough space" error on
40 40 # writing large size of data to console.
41 41 limit = 16000
42 42 l = len(s)
43 43 start = 0
44 44 self.softspace = 0;
45 45 while start < l:
46 46 end = start + limit
47 47 self.fp.write(s[start:end])
48 48 start = end
49 49 except IOError, inst:
50 50 if inst.errno != 0: raise
51 51 self.close()
52 52 raise IOError(errno.EPIPE, 'Broken pipe')
53 53
54 54 def flush(self):
55 55 try:
56 56 return self.fp.flush()
57 57 except IOError, inst:
58 58 if inst.errno != errno.EINVAL: raise
59 59 self.close()
60 60 raise IOError(errno.EPIPE, 'Broken pipe')
61 61
62 62 sys.stdout = winstdout(sys.stdout)
63 63
64 64 def _is_win_9x():
65 65 '''return true if run on windows 95, 98 or me.'''
66 66 try:
67 67 return sys.getwindowsversion()[3] == 1
68 68 except AttributeError:
69 69 return 'command' in os.environ.get('comspec', '')
70 70
71 71 def openhardlinks():
72 72 return not _is_win_9x() and "win32api" in globals()
73 73
74 74 def system_rcpath():
75 75 try:
76 76 return system_rcpath_win32()
77 77 except:
78 78 return [r'c:\mercurial\mercurial.ini']
79 79
80 80 def user_rcpath():
81 81 '''return os-specific hgrc search path to the user dir'''
82 82 try:
83 83 path = user_rcpath_win32()
84 84 except:
85 85 home = os.path.expanduser('~')
86 86 path = [os.path.join(home, 'mercurial.ini'),
87 87 os.path.join(home, '.hgrc')]
88 88 userprofile = os.environ.get('USERPROFILE')
89 89 if userprofile:
90 90 path.append(os.path.join(userprofile, 'mercurial.ini'))
91 91 path.append(os.path.join(userprofile, '.hgrc'))
92 92 return path
93 93
94 94 def parse_patch_output(output_line):
95 95 """parses the output produced by patch and returns the filename"""
96 96 pf = output_line[14:]
97 97 if pf[0] == '`':
98 98 pf = pf[1:-1] # Remove the quotes
99 99 return pf
100 100
101 101 def sshargs(sshcmd, host, user, port):
102 102 '''Build argument list for ssh or Plink'''
103 103 pflag = 'plink' in sshcmd.lower() and '-P' or '-p'
104 104 args = user and ("%s@%s" % (user, host)) or host
105 105 return port and ("%s %s %s" % (args, pflag, port)) or args
106 106
107 107 def testpid(pid):
108 108 '''return False if pid dead, True if running or not known'''
109 109 return True
110 110
111 111 def set_flags(f, l, x):
112 112 pass
113 113
114 114 def set_binary(fd):
115 115 # When run without console, pipes may expose invalid
116 116 # fileno(), usually set to -1.
117 117 if hasattr(fd, 'fileno') and fd.fileno() >= 0:
118 118 msvcrt.setmode(fd.fileno(), os.O_BINARY)
119 119
120 120 def pconvert(path):
121 121 return '/'.join(path.split(os.sep))
122 122
123 123 def localpath(path):
124 124 return path.replace('/', '\\')
125 125
126 126 def normpath(path):
127 127 return pconvert(os.path.normpath(path))
128 128
129 129 def realpath(path):
130 130 '''
131 131 Returns the true, canonical file system path equivalent to the given
132 132 path.
133 133 '''
134 134 # TODO: There may be a more clever way to do this that also handles other,
135 135 # less common file systems.
136 136 return os.path.normpath(os.path.normcase(os.path.realpath(path)))
137 137
138 138 def samestat(s1, s2):
139 139 return False
140 140
141 141 # A sequence of backslashes is special iff it precedes a double quote:
142 142 # - if there's an even number of backslashes, the double quote is not
143 143 # quoted (i.e. it ends the quoted region)
144 144 # - if there's an odd number of backslashes, the double quote is quoted
145 145 # - in both cases, every pair of backslashes is unquoted into a single
146 146 # backslash
147 147 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
148 148 # So, to quote a string, we must surround it in double quotes, double
149 149 # the number of backslashes that preceed double quotes and add another
150 150 # backslash before every double quote (being careful with the double
151 151 # quote we've appended to the end)
152 152 _quotere = None
153 153 def shellquote(s):
154 154 global _quotere
155 155 if _quotere is None:
156 156 _quotere = re.compile(r'(\\*)("|\\$)')
157 157 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
158 158
159 159 def quotecommand(cmd):
160 160 """Build a command string suitable for os.popen* calls."""
161 161 # The extra quotes are needed because popen* runs the command
162 162 # through the current COMSPEC. cmd.exe suppress enclosing quotes.
163 163 return '"' + cmd + '"'
164 164
165 165 def popen(command, mode='r'):
166 166 # Work around "popen spawned process may not write to stdout
167 167 # under windows"
168 168 # http://bugs.python.org/issue1366
169 169 command += " 2> %s" % nulldev
170 170 return os.popen(quotecommand(command), mode)
171 171
172 172 def explain_exit(code):
173 173 return _("exited with status %d") % code, code
174 174
175 175 # if you change this stub into a real check, please try to implement the
176 176 # username and groupname functions above, too.
177 177 def isowner(st):
178 178 return True
179 179
180 180 def find_exe(command):
181 181 '''Find executable for command searching like cmd.exe does.
182 182 If command is a basename then PATH is searched for command.
183 183 PATH isn't searched if command is an absolute or relative path.
184 184 An extension from PATHEXT is found and added if not present.
185 185 If command isn't found None is returned.'''
186 186 pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
187 187 pathexts = [ext for ext in pathext.lower().split(os.pathsep)]
188 188 if os.path.splitext(command)[1].lower() in pathexts:
189 189 pathexts = ['']
190 190
191 191 def findexisting(pathcommand):
192 192 'Will append extension (if needed) and return existing file'
193 193 for ext in pathexts:
194 194 executable = pathcommand + ext
195 195 if os.path.exists(executable):
196 196 return executable
197 197 return None
198 198
199 199 if os.sep in command:
200 200 return findexisting(command)
201 201
202 202 for path in os.environ.get('PATH', '').split(os.pathsep):
203 203 executable = findexisting(os.path.join(path, command))
204 204 if executable is not None:
205 205 return executable
206 206 return None
207 207
208 208 def set_signal_handler():
209 209 try:
210 210 set_signal_handler_win32()
211 211 except NameError:
212 212 pass
213 213
214 214 def statfiles(files):
215 215 '''Stat each file in files and yield stat or None if file does not exist.
216 216 Cluster and cache stat per directory to minimize number of OS stat calls.'''
217 217 ncase = os.path.normcase
218 218 sep = os.sep
219 219 dircache = {} # dirname -> filename -> status | None if file does not exist
220 220 for nf in files:
221 221 nf = ncase(nf)
222 222 dir, base = os.path.split(nf)
223 223 if not dir:
224 224 dir = '.'
225 225 cache = dircache.get(dir, None)
226 226 if cache is None:
227 227 try:
228 228 dmap = dict([(ncase(n), s)
229 229 for n, k, s in osutil.listdir(dir, True)])
230 230 except OSError, err:
231 231 # handle directory not found in Python version prior to 2.5
232 232 # Python <= 2.4 returns native Windows code 3 in errno
233 233 # Python >= 2.5 returns ENOENT and adds winerror field
234 234 # EINVAL is raised if dir is not a directory.
235 235 if err.errno not in (3, errno.ENOENT, errno.EINVAL,
236 236 errno.ENOTDIR):
237 237 raise
238 238 dmap = {}
239 239 cache = dircache.setdefault(dir, dmap)
240 240 yield cache.get(base, None)
241 241
242 242 def getuser():
243 243 '''return name of current user'''
244 244 raise error.Abort(_('user name not available - set USERNAME '
245 245 'environment variable'))
246 246
247 247 def username(uid=None):
248 248 """Return the name of the user with the given uid.
249 249
250 250 If uid is None, return the name of the current user."""
251 251 return None
252 252
253 253 def groupname(gid=None):
254 254 """Return the name of the group with the given gid.
255 255
256 256 If gid is None, return the name of the current group."""
257 257 return None
258 258
259 259 def _removedirs(name):
260 260 """special version of os.removedirs that does not remove symlinked
261 261 directories or junction points if they actually contain files"""
262 262 if osutil.listdir(name):
263 263 return
264 264 os.rmdir(name)
265 265 head, tail = os.path.split(name)
266 266 if not tail:
267 267 head, tail = os.path.split(head)
268 268 while head and tail:
269 269 try:
270 if osutil.listdir(name):
270 if osutil.listdir(head):
271 271 return
272 272 os.rmdir(head)
273 273 except:
274 274 break
275 275 head, tail = os.path.split(head)
276 276
277 277 def unlink(f):
278 278 """unlink and remove the directory if it is empty"""
279 279 os.unlink(f)
280 280 # try removing directories that might now be empty
281 281 try:
282 282 _removedirs(os.path.dirname(f))
283 283 except OSError:
284 284 pass
285 285
286 286 try:
287 287 # override functions with win32 versions if possible
288 288 from win32 import *
289 289 except ImportError:
290 290 pass
291 291
292 292 expandglobs = True
@@ -1,347 +1,368
1 1 #!/bin/sh
2 2
3 3 hg init a
4 4 mkdir a/d1
5 5 mkdir a/d1/d2
6 6 echo line 1 > a/a
7 7 echo line 1 > a/d1/d2/a
8 8 hg --cwd a ci -Ama
9 9
10 10 echo line 2 >> a/a
11 11 hg --cwd a ci -u someone -d '1 0' -m'second change'
12 12
13 13 echo % import exported patch
14 14 hg clone -r0 a b
15 15 hg --cwd a export tip > tip.patch
16 16 hg --cwd b import ../tip.patch
17 17 echo % message should be same
18 18 hg --cwd b tip | grep 'second change'
19 19 echo % committer should be same
20 20 hg --cwd b tip | grep someone
21 21 rm -r b
22 22
23 23 echo % import exported patch with external patcher
24 24 cat > dummypatch.py <<EOF
25 25 print 'patching file a'
26 26 file('a', 'wb').write('line2\n')
27 27 EOF
28 28 chmod +x dummypatch.py
29 29 hg clone -r0 a b
30 30 hg --cwd a export tip > tip.patch
31 31 hg --config ui.patch='python ../dummypatch.py' --cwd b import ../tip.patch
32 32 cat b/a
33 33 rm -r b
34 34
35 35 echo % import of plain diff should fail without message
36 36 hg clone -r0 a b
37 37 hg --cwd a diff -r0:1 > tip.patch
38 38 hg --cwd b import ../tip.patch
39 39 rm -r b
40 40
41 41 echo % import of plain diff should be ok with message
42 42 hg clone -r0 a b
43 43 hg --cwd a diff -r0:1 > tip.patch
44 44 hg --cwd b import -mpatch ../tip.patch
45 45 rm -r b
46 46
47 47 echo % import of plain diff with specific date and user
48 48 hg clone -r0 a b
49 49 hg --cwd a diff -r0:1 > tip.patch
50 50 hg --cwd b import -mpatch -d '1 0' -u 'user@nowhere.net' ../tip.patch
51 51 hg -R b tip -pv
52 52 rm -r b
53 53
54 54 echo % import of plain diff should be ok with --no-commit
55 55 hg clone -r0 a b
56 56 hg --cwd a diff -r0:1 > tip.patch
57 57 hg --cwd b import --no-commit ../tip.patch
58 58 hg --cwd b diff --nodates
59 59 rm -r b
60 60
61 61 echo % hg -R repo import
62 62 # put the clone in a subdir - having a directory named "a"
63 63 # used to hide a bug.
64 64 mkdir dir
65 65 hg clone -r0 a dir/b
66 66 hg --cwd a export tip > dir/tip.patch
67 67 cd dir
68 68 hg -R b import tip.patch
69 69 cd ..
70 70 rm -r dir
71 71
72 72 echo % import from stdin
73 73 hg clone -r0 a b
74 74 hg --cwd a export tip | hg --cwd b import -
75 75 rm -r b
76 76
77 77 echo % override commit message
78 78 hg clone -r0 a b
79 79 hg --cwd a export tip | hg --cwd b import -m 'override' -
80 80 hg --cwd b tip | grep override
81 81 rm -r b
82 82
83 83 cat > mkmsg.py <<EOF
84 84 import email.Message, sys
85 85 msg = email.Message.Message()
86 86 msg.set_payload('email commit message\n' + open('tip.patch', 'rb').read())
87 87 msg['Subject'] = 'email patch'
88 88 msg['From'] = 'email patcher'
89 89 sys.stdout.write(msg.as_string())
90 90 EOF
91 91
92 92 echo % plain diff in email, subject, message body
93 93 hg clone -r0 a b
94 94 hg --cwd a diff -r0:1 > tip.patch
95 95 python mkmsg.py > msg.patch
96 96 hg --cwd b import ../msg.patch
97 97 hg --cwd b tip | grep email
98 98 rm -r b
99 99
100 100 echo % plain diff in email, no subject, message body
101 101 hg clone -r0 a b
102 102 grep -v '^Subject:' msg.patch | hg --cwd b import -
103 103 rm -r b
104 104
105 105 echo % plain diff in email, subject, no message body
106 106 hg clone -r0 a b
107 107 grep -v '^email ' msg.patch | hg --cwd b import -
108 108 rm -r b
109 109
110 110 echo % plain diff in email, no subject, no message body, should fail
111 111 hg clone -r0 a b
112 112 egrep -v '^(Subject|email)' msg.patch | hg --cwd b import -
113 113 rm -r b
114 114
115 115 echo % hg export in email, should use patch header
116 116 hg clone -r0 a b
117 117 hg --cwd a export tip > tip.patch
118 118 python mkmsg.py | hg --cwd b import -
119 119 hg --cwd b tip | grep second
120 120 rm -r b
121 121
122 122 # subject: duplicate detection, removal of [PATCH]
123 123 # The '---' tests the gitsendmail handling without proper mail headers
124 124 cat > mkmsg2.py <<EOF
125 125 import email.Message, sys
126 126 msg = email.Message.Message()
127 127 msg.set_payload('email patch\n\nnext line\n---\n' + open('tip.patch').read())
128 128 msg['Subject'] = '[PATCH] email patch'
129 129 msg['From'] = 'email patcher'
130 130 sys.stdout.write(msg.as_string())
131 131 EOF
132 132
133 133 echo '% plain diff in email, [PATCH] subject, message body with subject'
134 134 hg clone -r0 a b
135 135 hg --cwd a diff -r0:1 > tip.patch
136 136 python mkmsg2.py | hg --cwd b import -
137 137 hg --cwd b tip --template '{desc}\n'
138 138 rm -r b
139 139
140 140 # We weren't backing up the correct dirstate file when importing many patches
141 141 # (issue963)
142 142 echo '% import patch1 patch2; rollback'
143 143 echo line 3 >> a/a
144 144 hg --cwd a ci -m'third change'
145 145 hg --cwd a export -o '../patch%R' 1 2
146 146 hg clone -qr0 a b
147 147 hg --cwd b parents --template 'parent: {rev}\n'
148 148 hg --cwd b import ../patch1 ../patch2
149 149 hg --cwd b rollback
150 150 hg --cwd b parents --template 'parent: {rev}\n'
151 151 rm -r b
152 152
153 153 # bug non regression test
154 154 # importing a patch in a subdirectory failed at the commit stage
155 155 echo line 2 >> a/d1/d2/a
156 156 hg --cwd a ci -u someoneelse -d '1 0' -m'subdir change'
157 157 echo % hg import in a subdirectory
158 158 hg clone -r0 a b
159 159 hg --cwd a export tip | sed -e 's/d1\/d2\///' > tip.patch
160 160 dir=`pwd`
161 161 cd b/d1/d2 2>&1 > /dev/null
162 162 hg import ../../../tip.patch
163 163 cd $dir
164 164 echo "% message should be 'subdir change'"
165 165 hg --cwd b tip | grep 'subdir change'
166 166 echo "% committer should be 'someoneelse'"
167 167 hg --cwd b tip | grep someoneelse
168 168 echo "% should be empty"
169 169 hg --cwd b status
170 170
171 171
172 172 # Test fuzziness (ambiguous patch location, fuzz=2)
173 173 echo % test fuzziness
174 174 hg init fuzzy
175 175 cd fuzzy
176 176 echo line1 > a
177 177 echo line0 >> a
178 178 echo line3 >> a
179 179 hg ci -Am adda
180 180 echo line1 > a
181 181 echo line2 >> a
182 182 echo line0 >> a
183 183 echo line3 >> a
184 184 hg ci -m change a
185 185 hg export tip > tip.patch
186 186 hg up -C 0
187 187 echo line1 > a
188 188 echo line0 >> a
189 189 echo line1 >> a
190 190 echo line0 >> a
191 191 hg ci -m brancha
192 192 hg import -v tip.patch
193 193 cd ..
194 194
195 195 # Test hunk touching empty files (issue906)
196 196 hg init empty
197 197 cd empty
198 198 touch a
199 199 touch b1
200 200 touch c1
201 201 echo d > d
202 202 hg ci -Am init
203 203 echo a > a
204 204 echo b > b1
205 205 hg mv b1 b2
206 206 echo c > c1
207 207 hg copy c1 c2
208 208 rm d
209 209 touch d
210 210 hg diff --git
211 211 hg ci -m empty
212 212 hg export --git tip > empty.diff
213 213 hg up -C 0
214 214 hg import empty.diff
215 215 for name in a b1 b2 c1 c2 d;
216 216 do
217 217 echo % $name file
218 218 test -f $name && cat $name
219 219 done
220 220 cd ..
221 221
222 222 # Test importing a patch ending with a binary file removal
223 223 echo % test trailing binary removal
224 224 hg init binaryremoval
225 225 cd binaryremoval
226 226 echo a > a
227 227 python -c "file('b', 'wb').write('a\x00b')"
228 228 hg ci -Am addall
229 229 hg rm a
230 230 hg rm b
231 231 hg st
232 232 hg ci -m remove
233 233 hg export --git . > remove.diff
234 234 cat remove.diff | grep git
235 235 hg up -C 0
236 236 hg import remove.diff
237 237 hg manifest
238 238 cd ..
239 239
240 240 echo % 'test update+rename with common name (issue 927)'
241 241 hg init t
242 242 cd t
243 243 touch a
244 244 hg ci -Am t
245 245 echo a > a
246 246 # Here, bfile.startswith(afile)
247 247 hg copy a a2
248 248 hg ci -m copya
249 249 hg export --git tip > copy.diff
250 250 hg up -C 0
251 251 hg import copy.diff
252 252 echo % view a
253 253 # a should contain an 'a'
254 254 cat a
255 255 echo % view a2
256 256 # and a2 should have duplicated it
257 257 cat a2
258 258 cd ..
259 259
260 260 echo % 'test -p0'
261 261 hg init p0
262 262 cd p0
263 263 echo a > a
264 264 hg ci -Am t
265 265 hg import -p0 - << EOF
266 266 foobar
267 267 --- a Sat Apr 12 22:43:58 2008 -0400
268 268 +++ a Sat Apr 12 22:44:05 2008 -0400
269 269 @@ -1,1 +1,1 @@
270 270 -a
271 271 +bb
272 272 EOF
273 273 hg status
274 274 cat a
275 275 cd ..
276 276
277 277 echo % 'test paths outside repo root'
278 278 mkdir outside
279 279 touch outside/foo
280 280 hg init inside
281 281 cd inside
282 282 hg import - <<EOF
283 283 diff --git a/a b/b
284 284 rename from ../outside/foo
285 285 rename to bar
286 286 EOF
287 287 cd ..
288 288
289 289 echo '% test import with similarity (issue295)'
290 290 hg init sim
291 291 cd sim
292 292 echo 'this is a test' > a
293 293 hg ci -Ama
294 294 cat > ../rename.diff <<EOF
295 295 diff --git a/a b/a
296 296 deleted file mode 100644
297 297 --- a/a
298 298 +++ /dev/null
299 299 @@ -1,1 +0,0 @@
300 300 -this is a test
301 301 diff --git a/b b/b
302 302 new file mode 100644
303 303 --- /dev/null
304 304 +++ b/b
305 305 @@ -0,0 +1,2 @@
306 306 +this is a test
307 307 +foo
308 308 EOF
309 309 hg import --no-commit -v -s 1 ../rename.diff
310 310 hg st -C
311 311 hg revert -a
312 312 rm b
313 313 hg import --no-commit -v -s 100 ../rename.diff
314 314 hg st -C
315 315 cd ..
316 316
317 317
318 318 echo '% add empty file from the end of patch (issue 1495)'
319 319 hg init addemptyend
320 320 cd addemptyend
321 321 touch a
322 322 hg addremove
323 323 hg ci -m "commit"
324 324 cat > a.patch <<EOF
325 325 diff --git a/a b/a
326 326 --- a/a
327 327 +++ b/a
328 328 @@ -0,0 +1,1 @@
329 329 +a
330 330 diff --git a/b b/b
331 331 new file mode 100644
332 332 EOF
333 333 hg import --no-commit a.patch
334 334 cd ..
335 335
336 336 echo '% create file when source is not /dev/null'
337 337 cat > create.patch <<EOF
338 338 diff -Naur proj-orig/foo proj-new/foo
339 339 --- proj-orig/foo 1969-12-31 16:00:00.000000000 -0800
340 340 +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
341 341 @@ -0,0 +1,1 @@
342 342 +a
343 343 EOF
344 344 hg init oddcreate
345 345 cd oddcreate
346 346 hg import --no-commit ../create.patch
347 347 cat foo
348 cd ..
349
350 echo % 'first line mistaken for email headers (issue 1859)'
351 hg init emailconfusion
352 cd emailconfusion
353 cat > a.patch <<EOF
354 module: summary
355
356 description
357
358
359 diff -r 000000000000 -r 9b4c1e343b55 test.txt
360 --- /dev/null
361 +++ b/a
362 @@ -0,0 +1,1 @@
363 +a
364 EOF
365 hg import -d '0 0' a.patch
366 hg parents -v
367 cd ..
368
@@ -1,297 +1,310
1 1 adding a
2 2 adding d1/d2/a
3 3 % import exported patch
4 4 requesting all changes
5 5 adding changesets
6 6 adding manifests
7 7 adding file changes
8 8 added 1 changesets with 2 changes to 2 files
9 9 updating working directory
10 10 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
11 11 applying ../tip.patch
12 12 % message should be same
13 13 summary: second change
14 14 % committer should be same
15 15 user: someone
16 16 % import exported patch with external patcher
17 17 requesting all changes
18 18 adding changesets
19 19 adding manifests
20 20 adding file changes
21 21 added 1 changesets with 2 changes to 2 files
22 22 updating working directory
23 23 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
24 24 applying ../tip.patch
25 25 line2
26 26 % import of plain diff should fail without message
27 27 requesting all changes
28 28 adding changesets
29 29 adding manifests
30 30 adding file changes
31 31 added 1 changesets with 2 changes to 2 files
32 32 updating working directory
33 33 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 34 applying ../tip.patch
35 35 abort: empty commit message
36 36 % import of plain diff should be ok with message
37 37 requesting all changes
38 38 adding changesets
39 39 adding manifests
40 40 adding file changes
41 41 added 1 changesets with 2 changes to 2 files
42 42 updating working directory
43 43 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
44 44 applying ../tip.patch
45 45 % import of plain diff with specific date and user
46 46 requesting all changes
47 47 adding changesets
48 48 adding manifests
49 49 adding file changes
50 50 added 1 changesets with 2 changes to 2 files
51 51 updating working directory
52 52 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
53 53 applying ../tip.patch
54 54 changeset: 1:ca68f19f3a40
55 55 tag: tip
56 56 user: user@nowhere.net
57 57 date: Thu Jan 01 00:00:01 1970 +0000
58 58 files: a
59 59 description:
60 60 patch
61 61
62 62
63 63 diff -r 80971e65b431 -r ca68f19f3a40 a
64 64 --- a/a Thu Jan 01 00:00:00 1970 +0000
65 65 +++ b/a Thu Jan 01 00:00:01 1970 +0000
66 66 @@ -1,1 +1,2 @@
67 67 line 1
68 68 +line 2
69 69
70 70 % import of plain diff should be ok with --no-commit
71 71 requesting all changes
72 72 adding changesets
73 73 adding manifests
74 74 adding file changes
75 75 added 1 changesets with 2 changes to 2 files
76 76 updating working directory
77 77 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 78 applying ../tip.patch
79 79 diff -r 80971e65b431 a
80 80 --- a/a
81 81 +++ b/a
82 82 @@ -1,1 +1,2 @@
83 83 line 1
84 84 +line 2
85 85 % hg -R repo import
86 86 requesting all changes
87 87 adding changesets
88 88 adding manifests
89 89 adding file changes
90 90 added 1 changesets with 2 changes to 2 files
91 91 updating working directory
92 92 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
93 93 applying tip.patch
94 94 % import from stdin
95 95 requesting all changes
96 96 adding changesets
97 97 adding manifests
98 98 adding file changes
99 99 added 1 changesets with 2 changes to 2 files
100 100 updating working directory
101 101 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 102 applying patch from stdin
103 103 % override commit message
104 104 requesting all changes
105 105 adding changesets
106 106 adding manifests
107 107 adding file changes
108 108 added 1 changesets with 2 changes to 2 files
109 109 updating working directory
110 110 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
111 111 applying patch from stdin
112 112 summary: override
113 113 % plain diff in email, subject, message body
114 114 requesting all changes
115 115 adding changesets
116 116 adding manifests
117 117 adding file changes
118 118 added 1 changesets with 2 changes to 2 files
119 119 updating working directory
120 120 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
121 121 applying ../msg.patch
122 122 user: email patcher
123 123 summary: email patch
124 124 % plain diff in email, no subject, message body
125 125 requesting all changes
126 126 adding changesets
127 127 adding manifests
128 128 adding file changes
129 129 added 1 changesets with 2 changes to 2 files
130 130 updating working directory
131 131 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
132 132 applying patch from stdin
133 133 % plain diff in email, subject, no message body
134 134 requesting all changes
135 135 adding changesets
136 136 adding manifests
137 137 adding file changes
138 138 added 1 changesets with 2 changes to 2 files
139 139 updating working directory
140 140 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
141 141 applying patch from stdin
142 142 % plain diff in email, no subject, no message body, should fail
143 143 requesting all changes
144 144 adding changesets
145 145 adding manifests
146 146 adding file changes
147 147 added 1 changesets with 2 changes to 2 files
148 148 updating working directory
149 149 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
150 150 applying patch from stdin
151 151 abort: empty commit message
152 152 % hg export in email, should use patch header
153 153 requesting all changes
154 154 adding changesets
155 155 adding manifests
156 156 adding file changes
157 157 added 1 changesets with 2 changes to 2 files
158 158 updating working directory
159 159 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
160 160 applying patch from stdin
161 161 summary: second change
162 162 % plain diff in email, [PATCH] subject, message body with subject
163 163 requesting all changes
164 164 adding changesets
165 165 adding manifests
166 166 adding file changes
167 167 added 1 changesets with 2 changes to 2 files
168 168 updating working directory
169 169 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
170 170 applying patch from stdin
171 171 email patch
172 172
173 173 next line
174 174 ---
175 175 % import patch1 patch2; rollback
176 176 parent: 0
177 177 applying ../patch1
178 178 applying ../patch2
179 179 rolling back last transaction
180 180 parent: 1
181 181 % hg import in a subdirectory
182 182 requesting all changes
183 183 adding changesets
184 184 adding manifests
185 185 adding file changes
186 186 added 1 changesets with 2 changes to 2 files
187 187 updating working directory
188 188 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
189 189 applying ../../../tip.patch
190 190 % message should be 'subdir change'
191 191 summary: subdir change
192 192 % committer should be 'someoneelse'
193 193 user: someoneelse
194 194 % should be empty
195 195 % test fuzziness
196 196 adding a
197 197 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
198 198 created new head
199 199 applying tip.patch
200 200 patching file a
201 201 Hunk #1 succeeded at 1 with fuzz 2 (offset -2 lines).
202 202 a
203 203 adding a
204 204 adding b1
205 205 adding c1
206 206 adding d
207 207 diff --git a/a b/a
208 208 --- a/a
209 209 +++ b/a
210 210 @@ -0,0 +1,1 @@
211 211 +a
212 212 diff --git a/b1 b/b2
213 213 rename from b1
214 214 rename to b2
215 215 --- a/b1
216 216 +++ b/b2
217 217 @@ -0,0 +1,1 @@
218 218 +b
219 219 diff --git a/c1 b/c1
220 220 --- a/c1
221 221 +++ b/c1
222 222 @@ -0,0 +1,1 @@
223 223 +c
224 224 diff --git a/c1 b/c2
225 225 copy from c1
226 226 copy to c2
227 227 --- a/c1
228 228 +++ b/c2
229 229 @@ -0,0 +1,1 @@
230 230 +c
231 231 diff --git a/d b/d
232 232 --- a/d
233 233 +++ b/d
234 234 @@ -1,1 +0,0 @@
235 235 -d
236 236 4 files updated, 0 files merged, 2 files removed, 0 files unresolved
237 237 applying empty.diff
238 238 % a file
239 239 a
240 240 % b1 file
241 241 % b2 file
242 242 b
243 243 % c1 file
244 244 c
245 245 % c2 file
246 246 c
247 247 % d file
248 248 % test trailing binary removal
249 249 adding a
250 250 adding b
251 251 R a
252 252 R b
253 253 diff --git a/a b/a
254 254 diff --git a/b b/b
255 255 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
256 256 applying remove.diff
257 257 % test update+rename with common name (issue 927)
258 258 adding a
259 259 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
260 260 applying copy.diff
261 261 % view a
262 262 a
263 263 % view a2
264 264 a
265 265 % test -p0
266 266 adding a
267 267 applying patch from stdin
268 268 bb
269 269 % test paths outside repo root
270 270 applying patch from stdin
271 271 abort: ../outside/foo not under root
272 272 % test import with similarity (issue295)
273 273 adding a
274 274 applying ../rename.diff
275 275 patching file a
276 276 patching file b
277 277 removing a
278 278 adding b
279 279 recording removal of a as rename to b (88% similar)
280 280 A b
281 281 a
282 282 R a
283 283 undeleting a
284 284 forgetting b
285 285 applying ../rename.diff
286 286 patching file a
287 287 patching file b
288 288 removing a
289 289 adding b
290 290 A b
291 291 R a
292 292 % add empty file from the end of patch (issue 1495)
293 293 adding a
294 294 applying a.patch
295 295 % create file when source is not /dev/null
296 296 applying ../create.patch
297 297 a
298 % first line mistaken for email headers (issue 1859)
299 applying a.patch
300 changeset: 0:5a681217c0ad
301 tag: tip
302 user: test
303 date: Thu Jan 01 00:00:00 1970 +0000
304 files: a
305 description:
306 module: summary
307
308 description
309
310
@@ -1,110 +1,119
1 1 #!/bin/sh
2 2
3 3 remove() {
4 4 hg rm $@
5 5 hg st
6 6 # do not use ls -R, which recurses in .hg subdirs on Mac OS X 10.5
7 7 find . -name .hg -prune -o -type f -print | sort
8 8 hg up -C
9 9 }
10 10
11 11 hg init a
12 12 cd a
13 13 echo a > foo
14 14
15 15 echo % file not managed
16 16 remove foo
17 17
18 18 hg add foo
19 19 hg commit -m1
20 20
21 21 # the table cases
22 22
23 23 echo % 00 state added, options none
24 24 echo b > bar
25 25 hg add bar
26 26 remove bar
27 27
28 28 echo % 01 state clean, options none
29 29 remove foo
30 30
31 31 echo % 02 state modified, options none
32 32 echo b >> foo
33 33 remove foo
34 34
35 35 echo % 03 state missing, options none
36 36 rm foo
37 37 remove foo
38 38
39 39 echo % 10 state added, options -f
40 40 echo b > bar
41 41 hg add bar
42 42 remove -f bar
43 43 rm bar
44 44
45 45 echo % 11 state clean, options -f
46 46 remove -f foo
47 47
48 48 echo % 12 state modified, options -f
49 49 echo b >> foo
50 50 remove -f foo
51 51
52 52 echo % 13 state missing, options -f
53 53 rm foo
54 54 remove -f foo
55 55
56 56 echo % 20 state added, options -A
57 57 echo b > bar
58 58 hg add bar
59 59 remove -A bar
60 60
61 61 echo % 21 state clean, options -A
62 62 remove -A foo
63 63
64 64 echo % 22 state modified, options -A
65 65 echo b >> foo
66 66 remove -A foo
67 67
68 68 echo % 23 state missing, options -A
69 69 rm foo
70 70 remove -A foo
71 71
72 72 echo % 30 state added, options -Af
73 73 echo b > bar
74 74 hg add bar
75 75 remove -Af bar
76 76 rm bar
77 77
78 78 echo % 31 state clean, options -Af
79 79 remove -Af foo
80 80
81 81 echo % 32 state modified, options -Af
82 82 echo b >> foo
83 83 remove -Af foo
84 84
85 85 echo % 33 state missing, options -Af
86 86 rm foo
87 87 remove -Af foo
88 88
89 89 # test some directory stuff
90 90
91 91 mkdir test
92 92 echo a > test/foo
93 93 echo b > test/bar
94 94 hg ci -Am2
95 95
96 96 echo % dir, options none
97 97 rm test/bar
98 98 remove test
99 99
100 100 echo % dir, options -f
101 101 rm test/bar
102 102 remove -f test
103 103
104 104 echo % dir, options -A
105 105 rm test/bar
106 106 remove -A test
107 107
108 108 echo % dir, options -Af
109 109 rm test/bar
110 110 remove -Af test
111
112 echo 'test remove dropping empty trees (issue1861)'
113 mkdir -p issue1861/b/c
114 echo x > issue1861/x
115 echo y > issue1861/b/c/y
116 hg ci -Am add
117 hg rm issue1861/b
118 hg ci -m remove
119 ls issue1861
@@ -1,113 +1,118
1 1 % file not managed
2 2 not removing foo: file is untracked
3 3 ? foo
4 4 ./foo
5 5 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
6 6 % 00 state added, options none
7 7 not removing bar: file has been marked for add (use -f to force removal)
8 8 A bar
9 9 ./bar
10 10 ./foo
11 11 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
12 12 % 01 state clean, options none
13 13 R foo
14 14 ? bar
15 15 ./bar
16 16 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
17 17 % 02 state modified, options none
18 18 not removing foo: file is modified (use -f to force removal)
19 19 M foo
20 20 ? bar
21 21 ./bar
22 22 ./foo
23 23 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
24 24 % 03 state missing, options none
25 25 R foo
26 26 ? bar
27 27 ./bar
28 28 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
29 29 % 10 state added, options -f
30 30 ? bar
31 31 ./bar
32 32 ./foo
33 33 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 34 % 11 state clean, options -f
35 35 R foo
36 36 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
37 37 % 12 state modified, options -f
38 38 R foo
39 39 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
40 40 % 13 state missing, options -f
41 41 R foo
42 42 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
43 43 % 20 state added, options -A
44 44 not removing bar: file still exists (use -f to force removal)
45 45 A bar
46 46 ./bar
47 47 ./foo
48 48 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
49 49 % 21 state clean, options -A
50 50 not removing foo: file still exists (use -f to force removal)
51 51 ? bar
52 52 ./bar
53 53 ./foo
54 54 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
55 55 % 22 state modified, options -A
56 56 not removing foo: file still exists (use -f to force removal)
57 57 M foo
58 58 ? bar
59 59 ./bar
60 60 ./foo
61 61 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 62 % 23 state missing, options -A
63 63 R foo
64 64 ? bar
65 65 ./bar
66 66 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 67 % 30 state added, options -Af
68 68 ? bar
69 69 ./bar
70 70 ./foo
71 71 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
72 72 % 31 state clean, options -Af
73 73 R foo
74 74 ./foo
75 75 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
76 76 % 32 state modified, options -Af
77 77 R foo
78 78 ./foo
79 79 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
80 80 % 33 state missing, options -Af
81 81 R foo
82 82 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
83 83 adding test/bar
84 84 adding test/foo
85 85 % dir, options none
86 86 removing test/bar
87 87 removing test/foo
88 88 R test/bar
89 89 R test/foo
90 90 ./foo
91 91 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
92 92 % dir, options -f
93 93 removing test/bar
94 94 removing test/foo
95 95 R test/bar
96 96 R test/foo
97 97 ./foo
98 98 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
99 99 % dir, options -A
100 100 not removing test/foo: file still exists (use -f to force removal)
101 101 removing test/bar
102 102 R test/bar
103 103 ./foo
104 104 ./test/foo
105 105 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
106 106 % dir, options -Af
107 107 removing test/bar
108 108 removing test/foo
109 109 R test/bar
110 110 R test/foo
111 111 ./foo
112 112 ./test/foo
113 113 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
114 test remove dropping empty trees (issue1861)
115 adding issue1861/b/c/y
116 adding issue1861/x
117 removing issue1861/b/c/y
118 x
General Comments 0
You need to be logged in to leave comments. Login now