##// END OF EJS Templates
record: exiting editor with non-zero status should not stop recording session...
Laurent Charignon -
r25483:fb04372d default
parent child Browse files
Show More
@@ -1,2551 +1,2553 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import collections
10 10 import cStringIO, email, os, errno, re, posixpath, copy
11 11 import tempfile, zlib, shutil
12 12 # On python2.4 you have to import these by name or they fail to
13 13 # load. This was not a problem on Python 2.7.
14 14 import email.Generator
15 15 import email.Parser
16 16
17 17 from i18n import _
18 18 from node import hex, short
19 19 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
20 20 import pathutil
21 21
22 22 gitre = re.compile('diff --git a/(.*) b/(.*)')
23 23 tabsplitter = re.compile(r'(\t+|[^\t]+)')
24 24
25 25 class PatchError(Exception):
26 26 pass
27 27
28 28
29 29 # public functions
30 30
31 31 def split(stream):
32 32 '''return an iterator of individual patches from a stream'''
33 33 def isheader(line, inheader):
34 34 if inheader and line[0] in (' ', '\t'):
35 35 # continuation
36 36 return True
37 37 if line[0] in (' ', '-', '+'):
38 38 # diff line - don't check for header pattern in there
39 39 return False
40 40 l = line.split(': ', 1)
41 41 return len(l) == 2 and ' ' not in l[0]
42 42
43 43 def chunk(lines):
44 44 return cStringIO.StringIO(''.join(lines))
45 45
46 46 def hgsplit(stream, cur):
47 47 inheader = True
48 48
49 49 for line in stream:
50 50 if not line.strip():
51 51 inheader = False
52 52 if not inheader and line.startswith('# HG changeset patch'):
53 53 yield chunk(cur)
54 54 cur = []
55 55 inheader = True
56 56
57 57 cur.append(line)
58 58
59 59 if cur:
60 60 yield chunk(cur)
61 61
62 62 def mboxsplit(stream, cur):
63 63 for line in stream:
64 64 if line.startswith('From '):
65 65 for c in split(chunk(cur[1:])):
66 66 yield c
67 67 cur = []
68 68
69 69 cur.append(line)
70 70
71 71 if cur:
72 72 for c in split(chunk(cur[1:])):
73 73 yield c
74 74
75 75 def mimesplit(stream, cur):
76 76 def msgfp(m):
77 77 fp = cStringIO.StringIO()
78 78 g = email.Generator.Generator(fp, mangle_from_=False)
79 79 g.flatten(m)
80 80 fp.seek(0)
81 81 return fp
82 82
83 83 for line in stream:
84 84 cur.append(line)
85 85 c = chunk(cur)
86 86
87 87 m = email.Parser.Parser().parse(c)
88 88 if not m.is_multipart():
89 89 yield msgfp(m)
90 90 else:
91 91 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
92 92 for part in m.walk():
93 93 ct = part.get_content_type()
94 94 if ct not in ok_types:
95 95 continue
96 96 yield msgfp(part)
97 97
98 98 def headersplit(stream, cur):
99 99 inheader = False
100 100
101 101 for line in stream:
102 102 if not inheader and isheader(line, inheader):
103 103 yield chunk(cur)
104 104 cur = []
105 105 inheader = True
106 106 if inheader and not isheader(line, inheader):
107 107 inheader = False
108 108
109 109 cur.append(line)
110 110
111 111 if cur:
112 112 yield chunk(cur)
113 113
114 114 def remainder(cur):
115 115 yield chunk(cur)
116 116
117 117 class fiter(object):
118 118 def __init__(self, fp):
119 119 self.fp = fp
120 120
121 121 def __iter__(self):
122 122 return self
123 123
124 124 def next(self):
125 125 l = self.fp.readline()
126 126 if not l:
127 127 raise StopIteration
128 128 return l
129 129
130 130 inheader = False
131 131 cur = []
132 132
133 133 mimeheaders = ['content-type']
134 134
135 135 if not util.safehasattr(stream, 'next'):
136 136 # http responses, for example, have readline but not next
137 137 stream = fiter(stream)
138 138
139 139 for line in stream:
140 140 cur.append(line)
141 141 if line.startswith('# HG changeset patch'):
142 142 return hgsplit(stream, cur)
143 143 elif line.startswith('From '):
144 144 return mboxsplit(stream, cur)
145 145 elif isheader(line, inheader):
146 146 inheader = True
147 147 if line.split(':', 1)[0].lower() in mimeheaders:
148 148 # let email parser handle this
149 149 return mimesplit(stream, cur)
150 150 elif line.startswith('--- ') and inheader:
151 151 # No evil headers seen by diff start, split by hand
152 152 return headersplit(stream, cur)
153 153 # Not enough info, keep reading
154 154
155 155 # if we are here, we have a very plain patch
156 156 return remainder(cur)
157 157
158 158 def extract(ui, fileobj):
159 159 '''extract patch from data read from fileobj.
160 160
161 161 patch can be a normal patch or contained in an email message.
162 162
163 163 return tuple (filename, message, user, date, branch, node, p1, p2).
164 164 Any item in the returned tuple can be None. If filename is None,
165 165 fileobj did not contain a patch. Caller must unlink filename when done.'''
166 166
167 167 # attempt to detect the start of a patch
168 168 # (this heuristic is borrowed from quilt)
169 169 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
170 170 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
171 171 r'---[ \t].*?^\+\+\+[ \t]|'
172 172 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
173 173
174 174 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
175 175 tmpfp = os.fdopen(fd, 'w')
176 176 try:
177 177 msg = email.Parser.Parser().parse(fileobj)
178 178
179 179 subject = msg['Subject']
180 180 user = msg['From']
181 181 if not subject and not user:
182 182 # Not an email, restore parsed headers if any
183 183 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
184 184
185 185 # should try to parse msg['Date']
186 186 date = None
187 187 nodeid = None
188 188 branch = None
189 189 parents = []
190 190
191 191 if subject:
192 192 if subject.startswith('[PATCH'):
193 193 pend = subject.find(']')
194 194 if pend >= 0:
195 195 subject = subject[pend + 1:].lstrip()
196 196 subject = re.sub(r'\n[ \t]+', ' ', subject)
197 197 ui.debug('Subject: %s\n' % subject)
198 198 if user:
199 199 ui.debug('From: %s\n' % user)
200 200 diffs_seen = 0
201 201 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
202 202 message = ''
203 203 for part in msg.walk():
204 204 content_type = part.get_content_type()
205 205 ui.debug('Content-Type: %s\n' % content_type)
206 206 if content_type not in ok_types:
207 207 continue
208 208 payload = part.get_payload(decode=True)
209 209 m = diffre.search(payload)
210 210 if m:
211 211 hgpatch = False
212 212 hgpatchheader = False
213 213 ignoretext = False
214 214
215 215 ui.debug('found patch at byte %d\n' % m.start(0))
216 216 diffs_seen += 1
217 217 cfp = cStringIO.StringIO()
218 218 for line in payload[:m.start(0)].splitlines():
219 219 if line.startswith('# HG changeset patch') and not hgpatch:
220 220 ui.debug('patch generated by hg export\n')
221 221 hgpatch = True
222 222 hgpatchheader = True
223 223 # drop earlier commit message content
224 224 cfp.seek(0)
225 225 cfp.truncate()
226 226 subject = None
227 227 elif hgpatchheader:
228 228 if line.startswith('# User '):
229 229 user = line[7:]
230 230 ui.debug('From: %s\n' % user)
231 231 elif line.startswith("# Date "):
232 232 date = line[7:]
233 233 elif line.startswith("# Branch "):
234 234 branch = line[9:]
235 235 elif line.startswith("# Node ID "):
236 236 nodeid = line[10:]
237 237 elif line.startswith("# Parent "):
238 238 parents.append(line[9:].lstrip())
239 239 elif not line.startswith("# "):
240 240 hgpatchheader = False
241 241 elif line == '---':
242 242 ignoretext = True
243 243 if not hgpatchheader and not ignoretext:
244 244 cfp.write(line)
245 245 cfp.write('\n')
246 246 message = cfp.getvalue()
247 247 if tmpfp:
248 248 tmpfp.write(payload)
249 249 if not payload.endswith('\n'):
250 250 tmpfp.write('\n')
251 251 elif not diffs_seen and message and content_type == 'text/plain':
252 252 message += '\n' + payload
253 253 except: # re-raises
254 254 tmpfp.close()
255 255 os.unlink(tmpname)
256 256 raise
257 257
258 258 if subject and not message.startswith(subject):
259 259 message = '%s\n%s' % (subject, message)
260 260 tmpfp.close()
261 261 if not diffs_seen:
262 262 os.unlink(tmpname)
263 263 return None, message, user, date, branch, None, None, None
264 264
265 265 if parents:
266 266 p1 = parents.pop(0)
267 267 else:
268 268 p1 = None
269 269
270 270 if parents:
271 271 p2 = parents.pop(0)
272 272 else:
273 273 p2 = None
274 274
275 275 return tmpname, message, user, date, branch, nodeid, p1, p2
276 276
277 277 class patchmeta(object):
278 278 """Patched file metadata
279 279
280 280 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
281 281 or COPY. 'path' is patched file path. 'oldpath' is set to the
282 282 origin file when 'op' is either COPY or RENAME, None otherwise. If
283 283 file mode is changed, 'mode' is a tuple (islink, isexec) where
284 284 'islink' is True if the file is a symlink and 'isexec' is True if
285 285 the file is executable. Otherwise, 'mode' is None.
286 286 """
287 287 def __init__(self, path):
288 288 self.path = path
289 289 self.oldpath = None
290 290 self.mode = None
291 291 self.op = 'MODIFY'
292 292 self.binary = False
293 293
294 294 def setmode(self, mode):
295 295 islink = mode & 020000
296 296 isexec = mode & 0100
297 297 self.mode = (islink, isexec)
298 298
299 299 def copy(self):
300 300 other = patchmeta(self.path)
301 301 other.oldpath = self.oldpath
302 302 other.mode = self.mode
303 303 other.op = self.op
304 304 other.binary = self.binary
305 305 return other
306 306
307 307 def _ispatchinga(self, afile):
308 308 if afile == '/dev/null':
309 309 return self.op == 'ADD'
310 310 return afile == 'a/' + (self.oldpath or self.path)
311 311
312 312 def _ispatchingb(self, bfile):
313 313 if bfile == '/dev/null':
314 314 return self.op == 'DELETE'
315 315 return bfile == 'b/' + self.path
316 316
317 317 def ispatching(self, afile, bfile):
318 318 return self._ispatchinga(afile) and self._ispatchingb(bfile)
319 319
320 320 def __repr__(self):
321 321 return "<patchmeta %s %r>" % (self.op, self.path)
322 322
323 323 def readgitpatch(lr):
324 324 """extract git-style metadata about patches from <patchname>"""
325 325
326 326 # Filter patch for git information
327 327 gp = None
328 328 gitpatches = []
329 329 for line in lr:
330 330 line = line.rstrip(' \r\n')
331 331 if line.startswith('diff --git a/'):
332 332 m = gitre.match(line)
333 333 if m:
334 334 if gp:
335 335 gitpatches.append(gp)
336 336 dst = m.group(2)
337 337 gp = patchmeta(dst)
338 338 elif gp:
339 339 if line.startswith('--- '):
340 340 gitpatches.append(gp)
341 341 gp = None
342 342 continue
343 343 if line.startswith('rename from '):
344 344 gp.op = 'RENAME'
345 345 gp.oldpath = line[12:]
346 346 elif line.startswith('rename to '):
347 347 gp.path = line[10:]
348 348 elif line.startswith('copy from '):
349 349 gp.op = 'COPY'
350 350 gp.oldpath = line[10:]
351 351 elif line.startswith('copy to '):
352 352 gp.path = line[8:]
353 353 elif line.startswith('deleted file'):
354 354 gp.op = 'DELETE'
355 355 elif line.startswith('new file mode '):
356 356 gp.op = 'ADD'
357 357 gp.setmode(int(line[-6:], 8))
358 358 elif line.startswith('new mode '):
359 359 gp.setmode(int(line[-6:], 8))
360 360 elif line.startswith('GIT binary patch'):
361 361 gp.binary = True
362 362 if gp:
363 363 gitpatches.append(gp)
364 364
365 365 return gitpatches
366 366
367 367 class linereader(object):
368 368 # simple class to allow pushing lines back into the input stream
369 369 def __init__(self, fp):
370 370 self.fp = fp
371 371 self.buf = []
372 372
373 373 def push(self, line):
374 374 if line is not None:
375 375 self.buf.append(line)
376 376
377 377 def readline(self):
378 378 if self.buf:
379 379 l = self.buf[0]
380 380 del self.buf[0]
381 381 return l
382 382 return self.fp.readline()
383 383
384 384 def __iter__(self):
385 385 while True:
386 386 l = self.readline()
387 387 if not l:
388 388 break
389 389 yield l
390 390
391 391 class abstractbackend(object):
392 392 def __init__(self, ui):
393 393 self.ui = ui
394 394
395 395 def getfile(self, fname):
396 396 """Return target file data and flags as a (data, (islink,
397 397 isexec)) tuple. Data is None if file is missing/deleted.
398 398 """
399 399 raise NotImplementedError
400 400
401 401 def setfile(self, fname, data, mode, copysource):
402 402 """Write data to target file fname and set its mode. mode is a
403 403 (islink, isexec) tuple. If data is None, the file content should
404 404 be left unchanged. If the file is modified after being copied,
405 405 copysource is set to the original file name.
406 406 """
407 407 raise NotImplementedError
408 408
409 409 def unlink(self, fname):
410 410 """Unlink target file."""
411 411 raise NotImplementedError
412 412
413 413 def writerej(self, fname, failed, total, lines):
414 414 """Write rejected lines for fname. total is the number of hunks
415 415 which failed to apply and total the total number of hunks for this
416 416 files.
417 417 """
418 418 pass
419 419
420 420 def exists(self, fname):
421 421 raise NotImplementedError
422 422
423 423 class fsbackend(abstractbackend):
424 424 def __init__(self, ui, basedir):
425 425 super(fsbackend, self).__init__(ui)
426 426 self.opener = scmutil.opener(basedir)
427 427
428 428 def _join(self, f):
429 429 return os.path.join(self.opener.base, f)
430 430
431 431 def getfile(self, fname):
432 432 if self.opener.islink(fname):
433 433 return (self.opener.readlink(fname), (True, False))
434 434
435 435 isexec = False
436 436 try:
437 437 isexec = self.opener.lstat(fname).st_mode & 0100 != 0
438 438 except OSError, e:
439 439 if e.errno != errno.ENOENT:
440 440 raise
441 441 try:
442 442 return (self.opener.read(fname), (False, isexec))
443 443 except IOError, e:
444 444 if e.errno != errno.ENOENT:
445 445 raise
446 446 return None, None
447 447
448 448 def setfile(self, fname, data, mode, copysource):
449 449 islink, isexec = mode
450 450 if data is None:
451 451 self.opener.setflags(fname, islink, isexec)
452 452 return
453 453 if islink:
454 454 self.opener.symlink(data, fname)
455 455 else:
456 456 self.opener.write(fname, data)
457 457 if isexec:
458 458 self.opener.setflags(fname, False, True)
459 459
460 460 def unlink(self, fname):
461 461 self.opener.unlinkpath(fname, ignoremissing=True)
462 462
463 463 def writerej(self, fname, failed, total, lines):
464 464 fname = fname + ".rej"
465 465 self.ui.warn(
466 466 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
467 467 (failed, total, fname))
468 468 fp = self.opener(fname, 'w')
469 469 fp.writelines(lines)
470 470 fp.close()
471 471
472 472 def exists(self, fname):
473 473 return self.opener.lexists(fname)
474 474
475 475 class workingbackend(fsbackend):
476 476 def __init__(self, ui, repo, similarity):
477 477 super(workingbackend, self).__init__(ui, repo.root)
478 478 self.repo = repo
479 479 self.similarity = similarity
480 480 self.removed = set()
481 481 self.changed = set()
482 482 self.copied = []
483 483
484 484 def _checkknown(self, fname):
485 485 if self.repo.dirstate[fname] == '?' and self.exists(fname):
486 486 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
487 487
488 488 def setfile(self, fname, data, mode, copysource):
489 489 self._checkknown(fname)
490 490 super(workingbackend, self).setfile(fname, data, mode, copysource)
491 491 if copysource is not None:
492 492 self.copied.append((copysource, fname))
493 493 self.changed.add(fname)
494 494
495 495 def unlink(self, fname):
496 496 self._checkknown(fname)
497 497 super(workingbackend, self).unlink(fname)
498 498 self.removed.add(fname)
499 499 self.changed.add(fname)
500 500
501 501 def close(self):
502 502 wctx = self.repo[None]
503 503 changed = set(self.changed)
504 504 for src, dst in self.copied:
505 505 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
506 506 if self.removed:
507 507 wctx.forget(sorted(self.removed))
508 508 for f in self.removed:
509 509 if f not in self.repo.dirstate:
510 510 # File was deleted and no longer belongs to the
511 511 # dirstate, it was probably marked added then
512 512 # deleted, and should not be considered by
513 513 # marktouched().
514 514 changed.discard(f)
515 515 if changed:
516 516 scmutil.marktouched(self.repo, changed, self.similarity)
517 517 return sorted(self.changed)
518 518
519 519 class filestore(object):
520 520 def __init__(self, maxsize=None):
521 521 self.opener = None
522 522 self.files = {}
523 523 self.created = 0
524 524 self.maxsize = maxsize
525 525 if self.maxsize is None:
526 526 self.maxsize = 4*(2**20)
527 527 self.size = 0
528 528 self.data = {}
529 529
530 530 def setfile(self, fname, data, mode, copied=None):
531 531 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
532 532 self.data[fname] = (data, mode, copied)
533 533 self.size += len(data)
534 534 else:
535 535 if self.opener is None:
536 536 root = tempfile.mkdtemp(prefix='hg-patch-')
537 537 self.opener = scmutil.opener(root)
538 538 # Avoid filename issues with these simple names
539 539 fn = str(self.created)
540 540 self.opener.write(fn, data)
541 541 self.created += 1
542 542 self.files[fname] = (fn, mode, copied)
543 543
544 544 def getfile(self, fname):
545 545 if fname in self.data:
546 546 return self.data[fname]
547 547 if not self.opener or fname not in self.files:
548 548 return None, None, None
549 549 fn, mode, copied = self.files[fname]
550 550 return self.opener.read(fn), mode, copied
551 551
552 552 def close(self):
553 553 if self.opener:
554 554 shutil.rmtree(self.opener.base)
555 555
556 556 class repobackend(abstractbackend):
557 557 def __init__(self, ui, repo, ctx, store):
558 558 super(repobackend, self).__init__(ui)
559 559 self.repo = repo
560 560 self.ctx = ctx
561 561 self.store = store
562 562 self.changed = set()
563 563 self.removed = set()
564 564 self.copied = {}
565 565
566 566 def _checkknown(self, fname):
567 567 if fname not in self.ctx:
568 568 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
569 569
570 570 def getfile(self, fname):
571 571 try:
572 572 fctx = self.ctx[fname]
573 573 except error.LookupError:
574 574 return None, None
575 575 flags = fctx.flags()
576 576 return fctx.data(), ('l' in flags, 'x' in flags)
577 577
578 578 def setfile(self, fname, data, mode, copysource):
579 579 if copysource:
580 580 self._checkknown(copysource)
581 581 if data is None:
582 582 data = self.ctx[fname].data()
583 583 self.store.setfile(fname, data, mode, copysource)
584 584 self.changed.add(fname)
585 585 if copysource:
586 586 self.copied[fname] = copysource
587 587
588 588 def unlink(self, fname):
589 589 self._checkknown(fname)
590 590 self.removed.add(fname)
591 591
592 592 def exists(self, fname):
593 593 return fname in self.ctx
594 594
595 595 def close(self):
596 596 return self.changed | self.removed
597 597
598 598 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
599 599 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
600 600 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
601 601 eolmodes = ['strict', 'crlf', 'lf', 'auto']
602 602
603 603 class patchfile(object):
604 604 def __init__(self, ui, gp, backend, store, eolmode='strict'):
605 605 self.fname = gp.path
606 606 self.eolmode = eolmode
607 607 self.eol = None
608 608 self.backend = backend
609 609 self.ui = ui
610 610 self.lines = []
611 611 self.exists = False
612 612 self.missing = True
613 613 self.mode = gp.mode
614 614 self.copysource = gp.oldpath
615 615 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
616 616 self.remove = gp.op == 'DELETE'
617 617 if self.copysource is None:
618 618 data, mode = backend.getfile(self.fname)
619 619 else:
620 620 data, mode = store.getfile(self.copysource)[:2]
621 621 if data is not None:
622 622 self.exists = self.copysource is None or backend.exists(self.fname)
623 623 self.missing = False
624 624 if data:
625 625 self.lines = mdiff.splitnewlines(data)
626 626 if self.mode is None:
627 627 self.mode = mode
628 628 if self.lines:
629 629 # Normalize line endings
630 630 if self.lines[0].endswith('\r\n'):
631 631 self.eol = '\r\n'
632 632 elif self.lines[0].endswith('\n'):
633 633 self.eol = '\n'
634 634 if eolmode != 'strict':
635 635 nlines = []
636 636 for l in self.lines:
637 637 if l.endswith('\r\n'):
638 638 l = l[:-2] + '\n'
639 639 nlines.append(l)
640 640 self.lines = nlines
641 641 else:
642 642 if self.create:
643 643 self.missing = False
644 644 if self.mode is None:
645 645 self.mode = (False, False)
646 646 if self.missing:
647 647 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
648 648
649 649 self.hash = {}
650 650 self.dirty = 0
651 651 self.offset = 0
652 652 self.skew = 0
653 653 self.rej = []
654 654 self.fileprinted = False
655 655 self.printfile(False)
656 656 self.hunks = 0
657 657
658 658 def writelines(self, fname, lines, mode):
659 659 if self.eolmode == 'auto':
660 660 eol = self.eol
661 661 elif self.eolmode == 'crlf':
662 662 eol = '\r\n'
663 663 else:
664 664 eol = '\n'
665 665
666 666 if self.eolmode != 'strict' and eol and eol != '\n':
667 667 rawlines = []
668 668 for l in lines:
669 669 if l and l[-1] == '\n':
670 670 l = l[:-1] + eol
671 671 rawlines.append(l)
672 672 lines = rawlines
673 673
674 674 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
675 675
676 676 def printfile(self, warn):
677 677 if self.fileprinted:
678 678 return
679 679 if warn or self.ui.verbose:
680 680 self.fileprinted = True
681 681 s = _("patching file %s\n") % self.fname
682 682 if warn:
683 683 self.ui.warn(s)
684 684 else:
685 685 self.ui.note(s)
686 686
687 687
688 688 def findlines(self, l, linenum):
689 689 # looks through the hash and finds candidate lines. The
690 690 # result is a list of line numbers sorted based on distance
691 691 # from linenum
692 692
693 693 cand = self.hash.get(l, [])
694 694 if len(cand) > 1:
695 695 # resort our list of potentials forward then back.
696 696 cand.sort(key=lambda x: abs(x - linenum))
697 697 return cand
698 698
699 699 def write_rej(self):
700 700 # our rejects are a little different from patch(1). This always
701 701 # creates rejects in the same form as the original patch. A file
702 702 # header is inserted so that you can run the reject through patch again
703 703 # without having to type the filename.
704 704 if not self.rej:
705 705 return
706 706 base = os.path.basename(self.fname)
707 707 lines = ["--- %s\n+++ %s\n" % (base, base)]
708 708 for x in self.rej:
709 709 for l in x.hunk:
710 710 lines.append(l)
711 711 if l[-1] != '\n':
712 712 lines.append("\n\ No newline at end of file\n")
713 713 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
714 714
715 715 def apply(self, h):
716 716 if not h.complete():
717 717 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
718 718 (h.number, h.desc, len(h.a), h.lena, len(h.b),
719 719 h.lenb))
720 720
721 721 self.hunks += 1
722 722
723 723 if self.missing:
724 724 self.rej.append(h)
725 725 return -1
726 726
727 727 if self.exists and self.create:
728 728 if self.copysource:
729 729 self.ui.warn(_("cannot create %s: destination already "
730 730 "exists\n") % self.fname)
731 731 else:
732 732 self.ui.warn(_("file %s already exists\n") % self.fname)
733 733 self.rej.append(h)
734 734 return -1
735 735
736 736 if isinstance(h, binhunk):
737 737 if self.remove:
738 738 self.backend.unlink(self.fname)
739 739 else:
740 740 l = h.new(self.lines)
741 741 self.lines[:] = l
742 742 self.offset += len(l)
743 743 self.dirty = True
744 744 return 0
745 745
746 746 horig = h
747 747 if (self.eolmode in ('crlf', 'lf')
748 748 or self.eolmode == 'auto' and self.eol):
749 749 # If new eols are going to be normalized, then normalize
750 750 # hunk data before patching. Otherwise, preserve input
751 751 # line-endings.
752 752 h = h.getnormalized()
753 753
754 754 # fast case first, no offsets, no fuzz
755 755 old, oldstart, new, newstart = h.fuzzit(0, False)
756 756 oldstart += self.offset
757 757 orig_start = oldstart
758 758 # if there's skew we want to emit the "(offset %d lines)" even
759 759 # when the hunk cleanly applies at start + skew, so skip the
760 760 # fast case code
761 761 if (self.skew == 0 and
762 762 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
763 763 if self.remove:
764 764 self.backend.unlink(self.fname)
765 765 else:
766 766 self.lines[oldstart:oldstart + len(old)] = new
767 767 self.offset += len(new) - len(old)
768 768 self.dirty = True
769 769 return 0
770 770
771 771 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
772 772 self.hash = {}
773 773 for x, s in enumerate(self.lines):
774 774 self.hash.setdefault(s, []).append(x)
775 775
776 776 for fuzzlen in xrange(3):
777 777 for toponly in [True, False]:
778 778 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
779 779 oldstart = oldstart + self.offset + self.skew
780 780 oldstart = min(oldstart, len(self.lines))
781 781 if old:
782 782 cand = self.findlines(old[0][1:], oldstart)
783 783 else:
784 784 # Only adding lines with no or fuzzed context, just
785 785 # take the skew in account
786 786 cand = [oldstart]
787 787
788 788 for l in cand:
789 789 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
790 790 self.lines[l : l + len(old)] = new
791 791 self.offset += len(new) - len(old)
792 792 self.skew = l - orig_start
793 793 self.dirty = True
794 794 offset = l - orig_start - fuzzlen
795 795 if fuzzlen:
796 796 msg = _("Hunk #%d succeeded at %d "
797 797 "with fuzz %d "
798 798 "(offset %d lines).\n")
799 799 self.printfile(True)
800 800 self.ui.warn(msg %
801 801 (h.number, l + 1, fuzzlen, offset))
802 802 else:
803 803 msg = _("Hunk #%d succeeded at %d "
804 804 "(offset %d lines).\n")
805 805 self.ui.note(msg % (h.number, l + 1, offset))
806 806 return fuzzlen
807 807 self.printfile(True)
808 808 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
809 809 self.rej.append(horig)
810 810 return -1
811 811
812 812 def close(self):
813 813 if self.dirty:
814 814 self.writelines(self.fname, self.lines, self.mode)
815 815 self.write_rej()
816 816 return len(self.rej)
817 817
818 818 class header(object):
819 819 """patch header
820 820 """
821 821 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
822 822 diff_re = re.compile('diff -r .* (.*)$')
823 823 allhunks_re = re.compile('(?:index|deleted file) ')
824 824 pretty_re = re.compile('(?:new file|deleted file) ')
825 825 special_re = re.compile('(?:index|deleted|copy|rename) ')
826 826 newfile_re = re.compile('(?:new file)')
827 827
828 828 def __init__(self, header):
829 829 self.header = header
830 830 self.hunks = []
831 831
832 832 def binary(self):
833 833 return any(h.startswith('index ') for h in self.header)
834 834
835 835 def pretty(self, fp):
836 836 for h in self.header:
837 837 if h.startswith('index '):
838 838 fp.write(_('this modifies a binary file (all or nothing)\n'))
839 839 break
840 840 if self.pretty_re.match(h):
841 841 fp.write(h)
842 842 if self.binary():
843 843 fp.write(_('this is a binary file\n'))
844 844 break
845 845 if h.startswith('---'):
846 846 fp.write(_('%d hunks, %d lines changed\n') %
847 847 (len(self.hunks),
848 848 sum([max(h.added, h.removed) for h in self.hunks])))
849 849 break
850 850 fp.write(h)
851 851
852 852 def write(self, fp):
853 853 fp.write(''.join(self.header))
854 854
855 855 def allhunks(self):
856 856 return any(self.allhunks_re.match(h) for h in self.header)
857 857
858 858 def files(self):
859 859 match = self.diffgit_re.match(self.header[0])
860 860 if match:
861 861 fromfile, tofile = match.groups()
862 862 if fromfile == tofile:
863 863 return [fromfile]
864 864 return [fromfile, tofile]
865 865 else:
866 866 return self.diff_re.match(self.header[0]).groups()
867 867
868 868 def filename(self):
869 869 return self.files()[-1]
870 870
871 871 def __repr__(self):
872 872 return '<header %s>' % (' '.join(map(repr, self.files())))
873 873
874 874 def isnewfile(self):
875 875 return any(self.newfile_re.match(h) for h in self.header)
876 876
877 877 def special(self):
878 878 # Special files are shown only at the header level and not at the hunk
879 879 # level for example a file that has been deleted is a special file.
880 880 # The user cannot change the content of the operation, in the case of
881 881 # the deleted file he has to take the deletion or not take it, he
882 882 # cannot take some of it.
883 883 # Newly added files are special if they are empty, they are not special
884 884 # if they have some content as we want to be able to change it
885 885 nocontent = len(self.header) == 2
886 886 emptynewfile = self.isnewfile() and nocontent
887 887 return emptynewfile or \
888 888 any(self.special_re.match(h) for h in self.header)
889 889
890 890 class recordhunk(object):
891 891 """patch hunk
892 892
893 893 XXX shouldn't we merge this with the other hunk class?
894 894 """
895 895 maxcontext = 3
896 896
897 897 def __init__(self, header, fromline, toline, proc, before, hunk, after):
898 898 def trimcontext(number, lines):
899 899 delta = len(lines) - self.maxcontext
900 900 if False and delta > 0:
901 901 return number + delta, lines[:self.maxcontext]
902 902 return number, lines
903 903
904 904 self.header = header
905 905 self.fromline, self.before = trimcontext(fromline, before)
906 906 self.toline, self.after = trimcontext(toline, after)
907 907 self.proc = proc
908 908 self.hunk = hunk
909 909 self.added, self.removed = self.countchanges(self.hunk)
910 910
911 911 def __eq__(self, v):
912 912 if not isinstance(v, recordhunk):
913 913 return False
914 914
915 915 return ((v.hunk == self.hunk) and
916 916 (v.proc == self.proc) and
917 917 (self.fromline == v.fromline) and
918 918 (self.header.files() == v.header.files()))
919 919
920 920 def __hash__(self):
921 921 return hash((tuple(self.hunk),
922 922 tuple(self.header.files()),
923 923 self.fromline,
924 924 self.proc))
925 925
926 926 def countchanges(self, hunk):
927 927 """hunk -> (n+,n-)"""
928 928 add = len([h for h in hunk if h[0] == '+'])
929 929 rem = len([h for h in hunk if h[0] == '-'])
930 930 return add, rem
931 931
932 932 def write(self, fp):
933 933 delta = len(self.before) + len(self.after)
934 934 if self.after and self.after[-1] == '\\ No newline at end of file\n':
935 935 delta -= 1
936 936 fromlen = delta + self.removed
937 937 tolen = delta + self.added
938 938 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
939 939 (self.fromline, fromlen, self.toline, tolen,
940 940 self.proc and (' ' + self.proc)))
941 941 fp.write(''.join(self.before + self.hunk + self.after))
942 942
943 943 pretty = write
944 944
945 945 def filename(self):
946 946 return self.header.filename()
947 947
948 948 def __repr__(self):
949 949 return '<hunk %r@%d>' % (self.filename(), self.fromline)
950 950
951 951 def filterpatch(ui, headers, operation=None):
952 952 """Interactively filter patch chunks into applied-only chunks"""
953 953 if operation is None:
954 954 operation = _('record')
955 955
956 956 def prompt(skipfile, skipall, query, chunk):
957 957 """prompt query, and process base inputs
958 958
959 959 - y/n for the rest of file
960 960 - y/n for the rest
961 961 - ? (help)
962 962 - q (quit)
963 963
964 964 Return True/False and possibly updated skipfile and skipall.
965 965 """
966 966 newpatches = None
967 967 if skipall is not None:
968 968 return skipall, skipfile, skipall, newpatches
969 969 if skipfile is not None:
970 970 return skipfile, skipfile, skipall, newpatches
971 971 while True:
972 972 resps = _('[Ynesfdaq?]'
973 973 '$$ &Yes, record this change'
974 974 '$$ &No, skip this change'
975 975 '$$ &Edit this change manually'
976 976 '$$ &Skip remaining changes to this file'
977 977 '$$ Record remaining changes to this &file'
978 978 '$$ &Done, skip remaining changes and files'
979 979 '$$ Record &all changes to all remaining files'
980 980 '$$ &Quit, recording no changes'
981 981 '$$ &? (display help)')
982 982 r = ui.promptchoice("%s %s" % (query, resps))
983 983 ui.write("\n")
984 984 if r == 8: # ?
985 985 for c, t in ui.extractchoices(resps)[1]:
986 986 ui.write('%s - %s\n' % (c, t.lower()))
987 987 continue
988 988 elif r == 0: # yes
989 989 ret = True
990 990 elif r == 1: # no
991 991 ret = False
992 992 elif r == 2: # Edit patch
993 993 if chunk is None:
994 994 ui.write(_('cannot edit patch for whole file'))
995 995 ui.write("\n")
996 996 continue
997 997 if chunk.header.binary():
998 998 ui.write(_('cannot edit patch for binary file'))
999 999 ui.write("\n")
1000 1000 continue
1001 1001 # Patch comment based on the Git one (based on comment at end of
1002 1002 # http://mercurial.selenic.com/wiki/RecordExtension)
1003 1003 phelp = '---' + _("""
1004 1004 To remove '-' lines, make them ' ' lines (context).
1005 1005 To remove '+' lines, delete them.
1006 1006 Lines starting with # will be removed from the patch.
1007 1007
1008 1008 If the patch applies cleanly, the edited hunk will immediately be
1009 1009 added to the record list. If it does not apply cleanly, a rejects
1010 1010 file will be generated: you can use that when you try again. If
1011 1011 all lines of the hunk are removed, then the edit is aborted and
1012 1012 the hunk is left unchanged.
1013 1013 """)
1014 1014 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1015 1015 suffix=".diff", text=True)
1016 1016 ncpatchfp = None
1017 1017 try:
1018 1018 # Write the initial patch
1019 1019 f = os.fdopen(patchfd, "w")
1020 1020 chunk.header.write(f)
1021 1021 chunk.write(f)
1022 1022 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1023 1023 f.close()
1024 1024 # Start the editor and wait for it to complete
1025 1025 editor = ui.geteditor()
1026 ui.system("%s \"%s\"" % (editor, patchfn),
1027 environ={'HGUSER': ui.username()},
1028 onerr=util.Abort, errprefix=_("edit failed"))
1026 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1027 environ={'HGUSER': ui.username()})
1028 if ret != 0:
1029 ui.warn(_("editor exited with exit code %d\n") % ret)
1030 continue
1029 1031 # Remove comment lines
1030 1032 patchfp = open(patchfn)
1031 1033 ncpatchfp = cStringIO.StringIO()
1032 1034 for line in patchfp:
1033 1035 if not line.startswith('#'):
1034 1036 ncpatchfp.write(line)
1035 1037 patchfp.close()
1036 1038 ncpatchfp.seek(0)
1037 1039 newpatches = parsepatch(ncpatchfp)
1038 1040 finally:
1039 1041 os.unlink(patchfn)
1040 1042 del ncpatchfp
1041 1043 # Signal that the chunk shouldn't be applied as-is, but
1042 1044 # provide the new patch to be used instead.
1043 1045 ret = False
1044 1046 elif r == 3: # Skip
1045 1047 ret = skipfile = False
1046 1048 elif r == 4: # file (Record remaining)
1047 1049 ret = skipfile = True
1048 1050 elif r == 5: # done, skip remaining
1049 1051 ret = skipall = False
1050 1052 elif r == 6: # all
1051 1053 ret = skipall = True
1052 1054 elif r == 7: # quit
1053 1055 raise util.Abort(_('user quit'))
1054 1056 return ret, skipfile, skipall, newpatches
1055 1057
1056 1058 seen = set()
1057 1059 applied = {} # 'filename' -> [] of chunks
1058 1060 skipfile, skipall = None, None
1059 1061 pos, total = 1, sum(len(h.hunks) for h in headers)
1060 1062 for h in headers:
1061 1063 pos += len(h.hunks)
1062 1064 skipfile = None
1063 1065 fixoffset = 0
1064 1066 hdr = ''.join(h.header)
1065 1067 if hdr in seen:
1066 1068 continue
1067 1069 seen.add(hdr)
1068 1070 if skipall is None:
1069 1071 h.pretty(ui)
1070 1072 msg = (_('examine changes to %s?') %
1071 1073 _(' and ').join("'%s'" % f for f in h.files()))
1072 1074 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1073 1075 if not r:
1074 1076 continue
1075 1077 applied[h.filename()] = [h]
1076 1078 if h.allhunks():
1077 1079 applied[h.filename()] += h.hunks
1078 1080 continue
1079 1081 for i, chunk in enumerate(h.hunks):
1080 1082 if skipfile is None and skipall is None:
1081 1083 chunk.pretty(ui)
1082 1084 if total == 1:
1083 1085 msg = _("record this change to '%s'?") % chunk.filename()
1084 1086 else:
1085 1087 idx = pos - len(h.hunks) + i
1086 1088 msg = _("record change %d/%d to '%s'?") % (idx, total,
1087 1089 chunk.filename())
1088 1090 r, skipfile, skipall, newpatches = prompt(skipfile,
1089 1091 skipall, msg, chunk)
1090 1092 if r:
1091 1093 if fixoffset:
1092 1094 chunk = copy.copy(chunk)
1093 1095 chunk.toline += fixoffset
1094 1096 applied[chunk.filename()].append(chunk)
1095 1097 elif newpatches is not None:
1096 1098 for newpatch in newpatches:
1097 1099 for newhunk in newpatch.hunks:
1098 1100 if fixoffset:
1099 1101 newhunk.toline += fixoffset
1100 1102 applied[newhunk.filename()].append(newhunk)
1101 1103 else:
1102 1104 fixoffset += chunk.removed - chunk.added
1103 1105 return sum([h for h in applied.itervalues()
1104 1106 if h[0].special() or len(h) > 1], [])
1105 1107 class hunk(object):
1106 1108 def __init__(self, desc, num, lr, context):
1107 1109 self.number = num
1108 1110 self.desc = desc
1109 1111 self.hunk = [desc]
1110 1112 self.a = []
1111 1113 self.b = []
1112 1114 self.starta = self.lena = None
1113 1115 self.startb = self.lenb = None
1114 1116 if lr is not None:
1115 1117 if context:
1116 1118 self.read_context_hunk(lr)
1117 1119 else:
1118 1120 self.read_unified_hunk(lr)
1119 1121
1120 1122 def getnormalized(self):
1121 1123 """Return a copy with line endings normalized to LF."""
1122 1124
1123 1125 def normalize(lines):
1124 1126 nlines = []
1125 1127 for line in lines:
1126 1128 if line.endswith('\r\n'):
1127 1129 line = line[:-2] + '\n'
1128 1130 nlines.append(line)
1129 1131 return nlines
1130 1132
1131 1133 # Dummy object, it is rebuilt manually
1132 1134 nh = hunk(self.desc, self.number, None, None)
1133 1135 nh.number = self.number
1134 1136 nh.desc = self.desc
1135 1137 nh.hunk = self.hunk
1136 1138 nh.a = normalize(self.a)
1137 1139 nh.b = normalize(self.b)
1138 1140 nh.starta = self.starta
1139 1141 nh.startb = self.startb
1140 1142 nh.lena = self.lena
1141 1143 nh.lenb = self.lenb
1142 1144 return nh
1143 1145
1144 1146 def read_unified_hunk(self, lr):
1145 1147 m = unidesc.match(self.desc)
1146 1148 if not m:
1147 1149 raise PatchError(_("bad hunk #%d") % self.number)
1148 1150 self.starta, self.lena, self.startb, self.lenb = m.groups()
1149 1151 if self.lena is None:
1150 1152 self.lena = 1
1151 1153 else:
1152 1154 self.lena = int(self.lena)
1153 1155 if self.lenb is None:
1154 1156 self.lenb = 1
1155 1157 else:
1156 1158 self.lenb = int(self.lenb)
1157 1159 self.starta = int(self.starta)
1158 1160 self.startb = int(self.startb)
1159 1161 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1160 1162 self.b)
1161 1163 # if we hit eof before finishing out the hunk, the last line will
1162 1164 # be zero length. Lets try to fix it up.
1163 1165 while len(self.hunk[-1]) == 0:
1164 1166 del self.hunk[-1]
1165 1167 del self.a[-1]
1166 1168 del self.b[-1]
1167 1169 self.lena -= 1
1168 1170 self.lenb -= 1
1169 1171 self._fixnewline(lr)
1170 1172
1171 1173 def read_context_hunk(self, lr):
1172 1174 self.desc = lr.readline()
1173 1175 m = contextdesc.match(self.desc)
1174 1176 if not m:
1175 1177 raise PatchError(_("bad hunk #%d") % self.number)
1176 1178 self.starta, aend = m.groups()
1177 1179 self.starta = int(self.starta)
1178 1180 if aend is None:
1179 1181 aend = self.starta
1180 1182 self.lena = int(aend) - self.starta
1181 1183 if self.starta:
1182 1184 self.lena += 1
1183 1185 for x in xrange(self.lena):
1184 1186 l = lr.readline()
1185 1187 if l.startswith('---'):
1186 1188 # lines addition, old block is empty
1187 1189 lr.push(l)
1188 1190 break
1189 1191 s = l[2:]
1190 1192 if l.startswith('- ') or l.startswith('! '):
1191 1193 u = '-' + s
1192 1194 elif l.startswith(' '):
1193 1195 u = ' ' + s
1194 1196 else:
1195 1197 raise PatchError(_("bad hunk #%d old text line %d") %
1196 1198 (self.number, x))
1197 1199 self.a.append(u)
1198 1200 self.hunk.append(u)
1199 1201
1200 1202 l = lr.readline()
1201 1203 if l.startswith('\ '):
1202 1204 s = self.a[-1][:-1]
1203 1205 self.a[-1] = s
1204 1206 self.hunk[-1] = s
1205 1207 l = lr.readline()
1206 1208 m = contextdesc.match(l)
1207 1209 if not m:
1208 1210 raise PatchError(_("bad hunk #%d") % self.number)
1209 1211 self.startb, bend = m.groups()
1210 1212 self.startb = int(self.startb)
1211 1213 if bend is None:
1212 1214 bend = self.startb
1213 1215 self.lenb = int(bend) - self.startb
1214 1216 if self.startb:
1215 1217 self.lenb += 1
1216 1218 hunki = 1
1217 1219 for x in xrange(self.lenb):
1218 1220 l = lr.readline()
1219 1221 if l.startswith('\ '):
1220 1222 # XXX: the only way to hit this is with an invalid line range.
1221 1223 # The no-eol marker is not counted in the line range, but I
1222 1224 # guess there are diff(1) out there which behave differently.
1223 1225 s = self.b[-1][:-1]
1224 1226 self.b[-1] = s
1225 1227 self.hunk[hunki - 1] = s
1226 1228 continue
1227 1229 if not l:
1228 1230 # line deletions, new block is empty and we hit EOF
1229 1231 lr.push(l)
1230 1232 break
1231 1233 s = l[2:]
1232 1234 if l.startswith('+ ') or l.startswith('! '):
1233 1235 u = '+' + s
1234 1236 elif l.startswith(' '):
1235 1237 u = ' ' + s
1236 1238 elif len(self.b) == 0:
1237 1239 # line deletions, new block is empty
1238 1240 lr.push(l)
1239 1241 break
1240 1242 else:
1241 1243 raise PatchError(_("bad hunk #%d old text line %d") %
1242 1244 (self.number, x))
1243 1245 self.b.append(s)
1244 1246 while True:
1245 1247 if hunki >= len(self.hunk):
1246 1248 h = ""
1247 1249 else:
1248 1250 h = self.hunk[hunki]
1249 1251 hunki += 1
1250 1252 if h == u:
1251 1253 break
1252 1254 elif h.startswith('-'):
1253 1255 continue
1254 1256 else:
1255 1257 self.hunk.insert(hunki - 1, u)
1256 1258 break
1257 1259
1258 1260 if not self.a:
1259 1261 # this happens when lines were only added to the hunk
1260 1262 for x in self.hunk:
1261 1263 if x.startswith('-') or x.startswith(' '):
1262 1264 self.a.append(x)
1263 1265 if not self.b:
1264 1266 # this happens when lines were only deleted from the hunk
1265 1267 for x in self.hunk:
1266 1268 if x.startswith('+') or x.startswith(' '):
1267 1269 self.b.append(x[1:])
1268 1270 # @@ -start,len +start,len @@
1269 1271 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1270 1272 self.startb, self.lenb)
1271 1273 self.hunk[0] = self.desc
1272 1274 self._fixnewline(lr)
1273 1275
1274 1276 def _fixnewline(self, lr):
1275 1277 l = lr.readline()
1276 1278 if l.startswith('\ '):
1277 1279 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1278 1280 else:
1279 1281 lr.push(l)
1280 1282
1281 1283 def complete(self):
1282 1284 return len(self.a) == self.lena and len(self.b) == self.lenb
1283 1285
1284 1286 def _fuzzit(self, old, new, fuzz, toponly):
1285 1287 # this removes context lines from the top and bottom of list 'l'. It
1286 1288 # checks the hunk to make sure only context lines are removed, and then
1287 1289 # returns a new shortened list of lines.
1288 1290 fuzz = min(fuzz, len(old))
1289 1291 if fuzz:
1290 1292 top = 0
1291 1293 bot = 0
1292 1294 hlen = len(self.hunk)
1293 1295 for x in xrange(hlen - 1):
1294 1296 # the hunk starts with the @@ line, so use x+1
1295 1297 if self.hunk[x + 1][0] == ' ':
1296 1298 top += 1
1297 1299 else:
1298 1300 break
1299 1301 if not toponly:
1300 1302 for x in xrange(hlen - 1):
1301 1303 if self.hunk[hlen - bot - 1][0] == ' ':
1302 1304 bot += 1
1303 1305 else:
1304 1306 break
1305 1307
1306 1308 bot = min(fuzz, bot)
1307 1309 top = min(fuzz, top)
1308 1310 return old[top:len(old) - bot], new[top:len(new) - bot], top
1309 1311 return old, new, 0
1310 1312
1311 1313 def fuzzit(self, fuzz, toponly):
1312 1314 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1313 1315 oldstart = self.starta + top
1314 1316 newstart = self.startb + top
1315 1317 # zero length hunk ranges already have their start decremented
1316 1318 if self.lena and oldstart > 0:
1317 1319 oldstart -= 1
1318 1320 if self.lenb and newstart > 0:
1319 1321 newstart -= 1
1320 1322 return old, oldstart, new, newstart
1321 1323
1322 1324 class binhunk(object):
1323 1325 'A binary patch file.'
1324 1326 def __init__(self, lr, fname):
1325 1327 self.text = None
1326 1328 self.delta = False
1327 1329 self.hunk = ['GIT binary patch\n']
1328 1330 self._fname = fname
1329 1331 self._read(lr)
1330 1332
1331 1333 def complete(self):
1332 1334 return self.text is not None
1333 1335
1334 1336 def new(self, lines):
1335 1337 if self.delta:
1336 1338 return [applybindelta(self.text, ''.join(lines))]
1337 1339 return [self.text]
1338 1340
1339 1341 def _read(self, lr):
1340 1342 def getline(lr, hunk):
1341 1343 l = lr.readline()
1342 1344 hunk.append(l)
1343 1345 return l.rstrip('\r\n')
1344 1346
1345 1347 size = 0
1346 1348 while True:
1347 1349 line = getline(lr, self.hunk)
1348 1350 if not line:
1349 1351 raise PatchError(_('could not extract "%s" binary data')
1350 1352 % self._fname)
1351 1353 if line.startswith('literal '):
1352 1354 size = int(line[8:].rstrip())
1353 1355 break
1354 1356 if line.startswith('delta '):
1355 1357 size = int(line[6:].rstrip())
1356 1358 self.delta = True
1357 1359 break
1358 1360 dec = []
1359 1361 line = getline(lr, self.hunk)
1360 1362 while len(line) > 1:
1361 1363 l = line[0]
1362 1364 if l <= 'Z' and l >= 'A':
1363 1365 l = ord(l) - ord('A') + 1
1364 1366 else:
1365 1367 l = ord(l) - ord('a') + 27
1366 1368 try:
1367 1369 dec.append(base85.b85decode(line[1:])[:l])
1368 1370 except ValueError, e:
1369 1371 raise PatchError(_('could not decode "%s" binary patch: %s')
1370 1372 % (self._fname, str(e)))
1371 1373 line = getline(lr, self.hunk)
1372 1374 text = zlib.decompress(''.join(dec))
1373 1375 if len(text) != size:
1374 1376 raise PatchError(_('"%s" length is %d bytes, should be %d')
1375 1377 % (self._fname, len(text), size))
1376 1378 self.text = text
1377 1379
1378 1380 def parsefilename(str):
1379 1381 # --- filename \t|space stuff
1380 1382 s = str[4:].rstrip('\r\n')
1381 1383 i = s.find('\t')
1382 1384 if i < 0:
1383 1385 i = s.find(' ')
1384 1386 if i < 0:
1385 1387 return s
1386 1388 return s[:i]
1387 1389
1388 1390 def reversehunks(hunks):
1389 1391 '''reverse the signs in the hunks given as argument
1390 1392
1391 1393 This function operates on hunks coming out of patch.filterpatch, that is
1392 1394 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1393 1395
1394 1396 >>> rawpatch = """diff --git a/folder1/g b/folder1/g
1395 1397 ... --- a/folder1/g
1396 1398 ... +++ b/folder1/g
1397 1399 ... @@ -1,7 +1,7 @@
1398 1400 ... +firstline
1399 1401 ... c
1400 1402 ... 1
1401 1403 ... 2
1402 1404 ... + 3
1403 1405 ... -4
1404 1406 ... 5
1405 1407 ... d
1406 1408 ... +lastline"""
1407 1409 >>> hunks = parsepatch(rawpatch)
1408 1410 >>> hunkscomingfromfilterpatch = []
1409 1411 >>> for h in hunks:
1410 1412 ... hunkscomingfromfilterpatch.append(h)
1411 1413 ... hunkscomingfromfilterpatch.extend(h.hunks)
1412 1414
1413 1415 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1414 1416 >>> fp = cStringIO.StringIO()
1415 1417 >>> for c in reversedhunks:
1416 1418 ... c.write(fp)
1417 1419 >>> fp.seek(0)
1418 1420 >>> reversedpatch = fp.read()
1419 1421 >>> print reversedpatch
1420 1422 diff --git a/folder1/g b/folder1/g
1421 1423 --- a/folder1/g
1422 1424 +++ b/folder1/g
1423 1425 @@ -1,4 +1,3 @@
1424 1426 -firstline
1425 1427 c
1426 1428 1
1427 1429 2
1428 1430 @@ -1,6 +2,6 @@
1429 1431 c
1430 1432 1
1431 1433 2
1432 1434 - 3
1433 1435 +4
1434 1436 5
1435 1437 d
1436 1438 @@ -5,3 +6,2 @@
1437 1439 5
1438 1440 d
1439 1441 -lastline
1440 1442
1441 1443 '''
1442 1444
1443 1445 import crecord as crecordmod
1444 1446 newhunks = []
1445 1447 for c in hunks:
1446 1448 if isinstance(c, crecordmod.uihunk):
1447 1449 # curses hunks encapsulate the record hunk in _hunk
1448 1450 c = c._hunk
1449 1451 if isinstance(c, recordhunk):
1450 1452 for j, line in enumerate(c.hunk):
1451 1453 if line.startswith("-"):
1452 1454 c.hunk[j] = "+" + c.hunk[j][1:]
1453 1455 elif line.startswith("+"):
1454 1456 c.hunk[j] = "-" + c.hunk[j][1:]
1455 1457 c.added, c.removed = c.removed, c.added
1456 1458 newhunks.append(c)
1457 1459 return newhunks
1458 1460
1459 1461 def parsepatch(originalchunks):
1460 1462 """patch -> [] of headers -> [] of hunks """
1461 1463 class parser(object):
1462 1464 """patch parsing state machine"""
1463 1465 def __init__(self):
1464 1466 self.fromline = 0
1465 1467 self.toline = 0
1466 1468 self.proc = ''
1467 1469 self.header = None
1468 1470 self.context = []
1469 1471 self.before = []
1470 1472 self.hunk = []
1471 1473 self.headers = []
1472 1474
1473 1475 def addrange(self, limits):
1474 1476 fromstart, fromend, tostart, toend, proc = limits
1475 1477 self.fromline = int(fromstart)
1476 1478 self.toline = int(tostart)
1477 1479 self.proc = proc
1478 1480
1479 1481 def addcontext(self, context):
1480 1482 if self.hunk:
1481 1483 h = recordhunk(self.header, self.fromline, self.toline,
1482 1484 self.proc, self.before, self.hunk, context)
1483 1485 self.header.hunks.append(h)
1484 1486 self.fromline += len(self.before) + h.removed
1485 1487 self.toline += len(self.before) + h.added
1486 1488 self.before = []
1487 1489 self.hunk = []
1488 1490 self.proc = ''
1489 1491 self.context = context
1490 1492
1491 1493 def addhunk(self, hunk):
1492 1494 if self.context:
1493 1495 self.before = self.context
1494 1496 self.context = []
1495 1497 self.hunk = hunk
1496 1498
1497 1499 def newfile(self, hdr):
1498 1500 self.addcontext([])
1499 1501 h = header(hdr)
1500 1502 self.headers.append(h)
1501 1503 self.header = h
1502 1504
1503 1505 def addother(self, line):
1504 1506 pass # 'other' lines are ignored
1505 1507
1506 1508 def finished(self):
1507 1509 self.addcontext([])
1508 1510 return self.headers
1509 1511
1510 1512 transitions = {
1511 1513 'file': {'context': addcontext,
1512 1514 'file': newfile,
1513 1515 'hunk': addhunk,
1514 1516 'range': addrange},
1515 1517 'context': {'file': newfile,
1516 1518 'hunk': addhunk,
1517 1519 'range': addrange,
1518 1520 'other': addother},
1519 1521 'hunk': {'context': addcontext,
1520 1522 'file': newfile,
1521 1523 'range': addrange},
1522 1524 'range': {'context': addcontext,
1523 1525 'hunk': addhunk},
1524 1526 'other': {'other': addother},
1525 1527 }
1526 1528
1527 1529 p = parser()
1528 1530 fp = cStringIO.StringIO()
1529 1531 fp.write(''.join(originalchunks))
1530 1532 fp.seek(0)
1531 1533
1532 1534 state = 'context'
1533 1535 for newstate, data in scanpatch(fp):
1534 1536 try:
1535 1537 p.transitions[state][newstate](p, data)
1536 1538 except KeyError:
1537 1539 raise PatchError('unhandled transition: %s -> %s' %
1538 1540 (state, newstate))
1539 1541 state = newstate
1540 1542 del fp
1541 1543 return p.finished()
1542 1544
1543 1545 def pathtransform(path, strip, prefix):
1544 1546 '''turn a path from a patch into a path suitable for the repository
1545 1547
1546 1548 prefix, if not empty, is expected to be normalized with a / at the end.
1547 1549
1548 1550 Returns (stripped components, path in repository).
1549 1551
1550 1552 >>> pathtransform('a/b/c', 0, '')
1551 1553 ('', 'a/b/c')
1552 1554 >>> pathtransform(' a/b/c ', 0, '')
1553 1555 ('', ' a/b/c')
1554 1556 >>> pathtransform(' a/b/c ', 2, '')
1555 1557 ('a/b/', 'c')
1556 1558 >>> pathtransform('a/b/c', 0, 'd/e/')
1557 1559 ('', 'd/e/a/b/c')
1558 1560 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1559 1561 ('a//b/', 'd/e/c')
1560 1562 >>> pathtransform('a/b/c', 3, '')
1561 1563 Traceback (most recent call last):
1562 1564 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1563 1565 '''
1564 1566 pathlen = len(path)
1565 1567 i = 0
1566 1568 if strip == 0:
1567 1569 return '', prefix + path.rstrip()
1568 1570 count = strip
1569 1571 while count > 0:
1570 1572 i = path.find('/', i)
1571 1573 if i == -1:
1572 1574 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1573 1575 (count, strip, path))
1574 1576 i += 1
1575 1577 # consume '//' in the path
1576 1578 while i < pathlen - 1 and path[i] == '/':
1577 1579 i += 1
1578 1580 count -= 1
1579 1581 return path[:i].lstrip(), prefix + path[i:].rstrip()
1580 1582
1581 1583 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1582 1584 nulla = afile_orig == "/dev/null"
1583 1585 nullb = bfile_orig == "/dev/null"
1584 1586 create = nulla and hunk.starta == 0 and hunk.lena == 0
1585 1587 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1586 1588 abase, afile = pathtransform(afile_orig, strip, prefix)
1587 1589 gooda = not nulla and backend.exists(afile)
1588 1590 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1589 1591 if afile == bfile:
1590 1592 goodb = gooda
1591 1593 else:
1592 1594 goodb = not nullb and backend.exists(bfile)
1593 1595 missing = not goodb and not gooda and not create
1594 1596
1595 1597 # some diff programs apparently produce patches where the afile is
1596 1598 # not /dev/null, but afile starts with bfile
1597 1599 abasedir = afile[:afile.rfind('/') + 1]
1598 1600 bbasedir = bfile[:bfile.rfind('/') + 1]
1599 1601 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1600 1602 and hunk.starta == 0 and hunk.lena == 0):
1601 1603 create = True
1602 1604 missing = False
1603 1605
1604 1606 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1605 1607 # diff is between a file and its backup. In this case, the original
1606 1608 # file should be patched (see original mpatch code).
1607 1609 isbackup = (abase == bbase and bfile.startswith(afile))
1608 1610 fname = None
1609 1611 if not missing:
1610 1612 if gooda and goodb:
1611 1613 if isbackup:
1612 1614 fname = afile
1613 1615 else:
1614 1616 fname = bfile
1615 1617 elif gooda:
1616 1618 fname = afile
1617 1619
1618 1620 if not fname:
1619 1621 if not nullb:
1620 1622 if isbackup:
1621 1623 fname = afile
1622 1624 else:
1623 1625 fname = bfile
1624 1626 elif not nulla:
1625 1627 fname = afile
1626 1628 else:
1627 1629 raise PatchError(_("undefined source and destination files"))
1628 1630
1629 1631 gp = patchmeta(fname)
1630 1632 if create:
1631 1633 gp.op = 'ADD'
1632 1634 elif remove:
1633 1635 gp.op = 'DELETE'
1634 1636 return gp
1635 1637
1636 1638 def scanpatch(fp):
1637 1639 """like patch.iterhunks, but yield different events
1638 1640
1639 1641 - ('file', [header_lines + fromfile + tofile])
1640 1642 - ('context', [context_lines])
1641 1643 - ('hunk', [hunk_lines])
1642 1644 - ('range', (-start,len, +start,len, proc))
1643 1645 """
1644 1646 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1645 1647 lr = linereader(fp)
1646 1648
1647 1649 def scanwhile(first, p):
1648 1650 """scan lr while predicate holds"""
1649 1651 lines = [first]
1650 1652 while True:
1651 1653 line = lr.readline()
1652 1654 if not line:
1653 1655 break
1654 1656 if p(line):
1655 1657 lines.append(line)
1656 1658 else:
1657 1659 lr.push(line)
1658 1660 break
1659 1661 return lines
1660 1662
1661 1663 while True:
1662 1664 line = lr.readline()
1663 1665 if not line:
1664 1666 break
1665 1667 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1666 1668 def notheader(line):
1667 1669 s = line.split(None, 1)
1668 1670 return not s or s[0] not in ('---', 'diff')
1669 1671 header = scanwhile(line, notheader)
1670 1672 fromfile = lr.readline()
1671 1673 if fromfile.startswith('---'):
1672 1674 tofile = lr.readline()
1673 1675 header += [fromfile, tofile]
1674 1676 else:
1675 1677 lr.push(fromfile)
1676 1678 yield 'file', header
1677 1679 elif line[0] == ' ':
1678 1680 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1679 1681 elif line[0] in '-+':
1680 1682 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1681 1683 else:
1682 1684 m = lines_re.match(line)
1683 1685 if m:
1684 1686 yield 'range', m.groups()
1685 1687 else:
1686 1688 yield 'other', line
1687 1689
1688 1690 def scangitpatch(lr, firstline):
1689 1691 """
1690 1692 Git patches can emit:
1691 1693 - rename a to b
1692 1694 - change b
1693 1695 - copy a to c
1694 1696 - change c
1695 1697
1696 1698 We cannot apply this sequence as-is, the renamed 'a' could not be
1697 1699 found for it would have been renamed already. And we cannot copy
1698 1700 from 'b' instead because 'b' would have been changed already. So
1699 1701 we scan the git patch for copy and rename commands so we can
1700 1702 perform the copies ahead of time.
1701 1703 """
1702 1704 pos = 0
1703 1705 try:
1704 1706 pos = lr.fp.tell()
1705 1707 fp = lr.fp
1706 1708 except IOError:
1707 1709 fp = cStringIO.StringIO(lr.fp.read())
1708 1710 gitlr = linereader(fp)
1709 1711 gitlr.push(firstline)
1710 1712 gitpatches = readgitpatch(gitlr)
1711 1713 fp.seek(pos)
1712 1714 return gitpatches
1713 1715
1714 1716 def iterhunks(fp):
1715 1717 """Read a patch and yield the following events:
1716 1718 - ("file", afile, bfile, firsthunk): select a new target file.
1717 1719 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1718 1720 "file" event.
1719 1721 - ("git", gitchanges): current diff is in git format, gitchanges
1720 1722 maps filenames to gitpatch records. Unique event.
1721 1723 """
1722 1724 afile = ""
1723 1725 bfile = ""
1724 1726 state = None
1725 1727 hunknum = 0
1726 1728 emitfile = newfile = False
1727 1729 gitpatches = None
1728 1730
1729 1731 # our states
1730 1732 BFILE = 1
1731 1733 context = None
1732 1734 lr = linereader(fp)
1733 1735
1734 1736 while True:
1735 1737 x = lr.readline()
1736 1738 if not x:
1737 1739 break
1738 1740 if state == BFILE and (
1739 1741 (not context and x[0] == '@')
1740 1742 or (context is not False and x.startswith('***************'))
1741 1743 or x.startswith('GIT binary patch')):
1742 1744 gp = None
1743 1745 if (gitpatches and
1744 1746 gitpatches[-1].ispatching(afile, bfile)):
1745 1747 gp = gitpatches.pop()
1746 1748 if x.startswith('GIT binary patch'):
1747 1749 h = binhunk(lr, gp.path)
1748 1750 else:
1749 1751 if context is None and x.startswith('***************'):
1750 1752 context = True
1751 1753 h = hunk(x, hunknum + 1, lr, context)
1752 1754 hunknum += 1
1753 1755 if emitfile:
1754 1756 emitfile = False
1755 1757 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1756 1758 yield 'hunk', h
1757 1759 elif x.startswith('diff --git a/'):
1758 1760 m = gitre.match(x.rstrip(' \r\n'))
1759 1761 if not m:
1760 1762 continue
1761 1763 if gitpatches is None:
1762 1764 # scan whole input for git metadata
1763 1765 gitpatches = scangitpatch(lr, x)
1764 1766 yield 'git', [g.copy() for g in gitpatches
1765 1767 if g.op in ('COPY', 'RENAME')]
1766 1768 gitpatches.reverse()
1767 1769 afile = 'a/' + m.group(1)
1768 1770 bfile = 'b/' + m.group(2)
1769 1771 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1770 1772 gp = gitpatches.pop()
1771 1773 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1772 1774 if not gitpatches:
1773 1775 raise PatchError(_('failed to synchronize metadata for "%s"')
1774 1776 % afile[2:])
1775 1777 gp = gitpatches[-1]
1776 1778 newfile = True
1777 1779 elif x.startswith('---'):
1778 1780 # check for a unified diff
1779 1781 l2 = lr.readline()
1780 1782 if not l2.startswith('+++'):
1781 1783 lr.push(l2)
1782 1784 continue
1783 1785 newfile = True
1784 1786 context = False
1785 1787 afile = parsefilename(x)
1786 1788 bfile = parsefilename(l2)
1787 1789 elif x.startswith('***'):
1788 1790 # check for a context diff
1789 1791 l2 = lr.readline()
1790 1792 if not l2.startswith('---'):
1791 1793 lr.push(l2)
1792 1794 continue
1793 1795 l3 = lr.readline()
1794 1796 lr.push(l3)
1795 1797 if not l3.startswith("***************"):
1796 1798 lr.push(l2)
1797 1799 continue
1798 1800 newfile = True
1799 1801 context = True
1800 1802 afile = parsefilename(x)
1801 1803 bfile = parsefilename(l2)
1802 1804
1803 1805 if newfile:
1804 1806 newfile = False
1805 1807 emitfile = True
1806 1808 state = BFILE
1807 1809 hunknum = 0
1808 1810
1809 1811 while gitpatches:
1810 1812 gp = gitpatches.pop()
1811 1813 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1812 1814
1813 1815 def applybindelta(binchunk, data):
1814 1816 """Apply a binary delta hunk
1815 1817 The algorithm used is the algorithm from git's patch-delta.c
1816 1818 """
1817 1819 def deltahead(binchunk):
1818 1820 i = 0
1819 1821 for c in binchunk:
1820 1822 i += 1
1821 1823 if not (ord(c) & 0x80):
1822 1824 return i
1823 1825 return i
1824 1826 out = ""
1825 1827 s = deltahead(binchunk)
1826 1828 binchunk = binchunk[s:]
1827 1829 s = deltahead(binchunk)
1828 1830 binchunk = binchunk[s:]
1829 1831 i = 0
1830 1832 while i < len(binchunk):
1831 1833 cmd = ord(binchunk[i])
1832 1834 i += 1
1833 1835 if (cmd & 0x80):
1834 1836 offset = 0
1835 1837 size = 0
1836 1838 if (cmd & 0x01):
1837 1839 offset = ord(binchunk[i])
1838 1840 i += 1
1839 1841 if (cmd & 0x02):
1840 1842 offset |= ord(binchunk[i]) << 8
1841 1843 i += 1
1842 1844 if (cmd & 0x04):
1843 1845 offset |= ord(binchunk[i]) << 16
1844 1846 i += 1
1845 1847 if (cmd & 0x08):
1846 1848 offset |= ord(binchunk[i]) << 24
1847 1849 i += 1
1848 1850 if (cmd & 0x10):
1849 1851 size = ord(binchunk[i])
1850 1852 i += 1
1851 1853 if (cmd & 0x20):
1852 1854 size |= ord(binchunk[i]) << 8
1853 1855 i += 1
1854 1856 if (cmd & 0x40):
1855 1857 size |= ord(binchunk[i]) << 16
1856 1858 i += 1
1857 1859 if size == 0:
1858 1860 size = 0x10000
1859 1861 offset_end = offset + size
1860 1862 out += data[offset:offset_end]
1861 1863 elif cmd != 0:
1862 1864 offset_end = i + cmd
1863 1865 out += binchunk[i:offset_end]
1864 1866 i += cmd
1865 1867 else:
1866 1868 raise PatchError(_('unexpected delta opcode 0'))
1867 1869 return out
1868 1870
1869 1871 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1870 1872 """Reads a patch from fp and tries to apply it.
1871 1873
1872 1874 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1873 1875 there was any fuzz.
1874 1876
1875 1877 If 'eolmode' is 'strict', the patch content and patched file are
1876 1878 read in binary mode. Otherwise, line endings are ignored when
1877 1879 patching then normalized according to 'eolmode'.
1878 1880 """
1879 1881 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1880 1882 prefix=prefix, eolmode=eolmode)
1881 1883
1882 1884 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1883 1885 eolmode='strict'):
1884 1886
1885 1887 if prefix:
1886 1888 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1887 1889 prefix)
1888 1890 if prefix != '':
1889 1891 prefix += '/'
1890 1892 def pstrip(p):
1891 1893 return pathtransform(p, strip - 1, prefix)[1]
1892 1894
1893 1895 rejects = 0
1894 1896 err = 0
1895 1897 current_file = None
1896 1898
1897 1899 for state, values in iterhunks(fp):
1898 1900 if state == 'hunk':
1899 1901 if not current_file:
1900 1902 continue
1901 1903 ret = current_file.apply(values)
1902 1904 if ret > 0:
1903 1905 err = 1
1904 1906 elif state == 'file':
1905 1907 if current_file:
1906 1908 rejects += current_file.close()
1907 1909 current_file = None
1908 1910 afile, bfile, first_hunk, gp = values
1909 1911 if gp:
1910 1912 gp.path = pstrip(gp.path)
1911 1913 if gp.oldpath:
1912 1914 gp.oldpath = pstrip(gp.oldpath)
1913 1915 else:
1914 1916 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1915 1917 prefix)
1916 1918 if gp.op == 'RENAME':
1917 1919 backend.unlink(gp.oldpath)
1918 1920 if not first_hunk:
1919 1921 if gp.op == 'DELETE':
1920 1922 backend.unlink(gp.path)
1921 1923 continue
1922 1924 data, mode = None, None
1923 1925 if gp.op in ('RENAME', 'COPY'):
1924 1926 data, mode = store.getfile(gp.oldpath)[:2]
1925 1927 # FIXME: failing getfile has never been handled here
1926 1928 assert data is not None
1927 1929 if gp.mode:
1928 1930 mode = gp.mode
1929 1931 if gp.op == 'ADD':
1930 1932 # Added files without content have no hunk and
1931 1933 # must be created
1932 1934 data = ''
1933 1935 if data or mode:
1934 1936 if (gp.op in ('ADD', 'RENAME', 'COPY')
1935 1937 and backend.exists(gp.path)):
1936 1938 raise PatchError(_("cannot create %s: destination "
1937 1939 "already exists") % gp.path)
1938 1940 backend.setfile(gp.path, data, mode, gp.oldpath)
1939 1941 continue
1940 1942 try:
1941 1943 current_file = patcher(ui, gp, backend, store,
1942 1944 eolmode=eolmode)
1943 1945 except PatchError, inst:
1944 1946 ui.warn(str(inst) + '\n')
1945 1947 current_file = None
1946 1948 rejects += 1
1947 1949 continue
1948 1950 elif state == 'git':
1949 1951 for gp in values:
1950 1952 path = pstrip(gp.oldpath)
1951 1953 data, mode = backend.getfile(path)
1952 1954 if data is None:
1953 1955 # The error ignored here will trigger a getfile()
1954 1956 # error in a place more appropriate for error
1955 1957 # handling, and will not interrupt the patching
1956 1958 # process.
1957 1959 pass
1958 1960 else:
1959 1961 store.setfile(path, data, mode)
1960 1962 else:
1961 1963 raise util.Abort(_('unsupported parser state: %s') % state)
1962 1964
1963 1965 if current_file:
1964 1966 rejects += current_file.close()
1965 1967
1966 1968 if rejects:
1967 1969 return -1
1968 1970 return err
1969 1971
1970 1972 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1971 1973 similarity):
1972 1974 """use <patcher> to apply <patchname> to the working directory.
1973 1975 returns whether patch was applied with fuzz factor."""
1974 1976
1975 1977 fuzz = False
1976 1978 args = []
1977 1979 cwd = repo.root
1978 1980 if cwd:
1979 1981 args.append('-d %s' % util.shellquote(cwd))
1980 1982 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1981 1983 util.shellquote(patchname)))
1982 1984 try:
1983 1985 for line in fp:
1984 1986 line = line.rstrip()
1985 1987 ui.note(line + '\n')
1986 1988 if line.startswith('patching file '):
1987 1989 pf = util.parsepatchoutput(line)
1988 1990 printed_file = False
1989 1991 files.add(pf)
1990 1992 elif line.find('with fuzz') >= 0:
1991 1993 fuzz = True
1992 1994 if not printed_file:
1993 1995 ui.warn(pf + '\n')
1994 1996 printed_file = True
1995 1997 ui.warn(line + '\n')
1996 1998 elif line.find('saving rejects to file') >= 0:
1997 1999 ui.warn(line + '\n')
1998 2000 elif line.find('FAILED') >= 0:
1999 2001 if not printed_file:
2000 2002 ui.warn(pf + '\n')
2001 2003 printed_file = True
2002 2004 ui.warn(line + '\n')
2003 2005 finally:
2004 2006 if files:
2005 2007 scmutil.marktouched(repo, files, similarity)
2006 2008 code = fp.close()
2007 2009 if code:
2008 2010 raise PatchError(_("patch command failed: %s") %
2009 2011 util.explainexit(code)[0])
2010 2012 return fuzz
2011 2013
2012 2014 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2013 2015 eolmode='strict'):
2014 2016 if files is None:
2015 2017 files = set()
2016 2018 if eolmode is None:
2017 2019 eolmode = ui.config('patch', 'eol', 'strict')
2018 2020 if eolmode.lower() not in eolmodes:
2019 2021 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
2020 2022 eolmode = eolmode.lower()
2021 2023
2022 2024 store = filestore()
2023 2025 try:
2024 2026 fp = open(patchobj, 'rb')
2025 2027 except TypeError:
2026 2028 fp = patchobj
2027 2029 try:
2028 2030 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2029 2031 eolmode=eolmode)
2030 2032 finally:
2031 2033 if fp != patchobj:
2032 2034 fp.close()
2033 2035 files.update(backend.close())
2034 2036 store.close()
2035 2037 if ret < 0:
2036 2038 raise PatchError(_('patch failed to apply'))
2037 2039 return ret > 0
2038 2040
2039 2041 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2040 2042 eolmode='strict', similarity=0):
2041 2043 """use builtin patch to apply <patchobj> to the working directory.
2042 2044 returns whether patch was applied with fuzz factor."""
2043 2045 backend = workingbackend(ui, repo, similarity)
2044 2046 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2045 2047
2046 2048 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2047 2049 eolmode='strict'):
2048 2050 backend = repobackend(ui, repo, ctx, store)
2049 2051 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2050 2052
2051 2053 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2052 2054 similarity=0):
2053 2055 """Apply <patchname> to the working directory.
2054 2056
2055 2057 'eolmode' specifies how end of lines should be handled. It can be:
2056 2058 - 'strict': inputs are read in binary mode, EOLs are preserved
2057 2059 - 'crlf': EOLs are ignored when patching and reset to CRLF
2058 2060 - 'lf': EOLs are ignored when patching and reset to LF
2059 2061 - None: get it from user settings, default to 'strict'
2060 2062 'eolmode' is ignored when using an external patcher program.
2061 2063
2062 2064 Returns whether patch was applied with fuzz factor.
2063 2065 """
2064 2066 patcher = ui.config('ui', 'patch')
2065 2067 if files is None:
2066 2068 files = set()
2067 2069 if patcher:
2068 2070 return _externalpatch(ui, repo, patcher, patchname, strip,
2069 2071 files, similarity)
2070 2072 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2071 2073 similarity)
2072 2074
2073 2075 def changedfiles(ui, repo, patchpath, strip=1):
2074 2076 backend = fsbackend(ui, repo.root)
2075 2077 fp = open(patchpath, 'rb')
2076 2078 try:
2077 2079 changed = set()
2078 2080 for state, values in iterhunks(fp):
2079 2081 if state == 'file':
2080 2082 afile, bfile, first_hunk, gp = values
2081 2083 if gp:
2082 2084 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2083 2085 if gp.oldpath:
2084 2086 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2085 2087 else:
2086 2088 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2087 2089 '')
2088 2090 changed.add(gp.path)
2089 2091 if gp.op == 'RENAME':
2090 2092 changed.add(gp.oldpath)
2091 2093 elif state not in ('hunk', 'git'):
2092 2094 raise util.Abort(_('unsupported parser state: %s') % state)
2093 2095 return changed
2094 2096 finally:
2095 2097 fp.close()
2096 2098
2097 2099 class GitDiffRequired(Exception):
2098 2100 pass
2099 2101
2100 2102 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2101 2103 '''return diffopts with all features supported and parsed'''
2102 2104 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2103 2105 git=True, whitespace=True, formatchanging=True)
2104 2106
2105 2107 diffopts = diffallopts
2106 2108
2107 2109 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2108 2110 whitespace=False, formatchanging=False):
2109 2111 '''return diffopts with only opted-in features parsed
2110 2112
2111 2113 Features:
2112 2114 - git: git-style diffs
2113 2115 - whitespace: whitespace options like ignoreblanklines and ignorews
2114 2116 - formatchanging: options that will likely break or cause correctness issues
2115 2117 with most diff parsers
2116 2118 '''
2117 2119 def get(key, name=None, getter=ui.configbool, forceplain=None):
2118 2120 if opts:
2119 2121 v = opts.get(key)
2120 2122 if v:
2121 2123 return v
2122 2124 if forceplain is not None and ui.plain():
2123 2125 return forceplain
2124 2126 return getter(section, name or key, None, untrusted=untrusted)
2125 2127
2126 2128 # core options, expected to be understood by every diff parser
2127 2129 buildopts = {
2128 2130 'nodates': get('nodates'),
2129 2131 'showfunc': get('show_function', 'showfunc'),
2130 2132 'context': get('unified', getter=ui.config),
2131 2133 }
2132 2134
2133 2135 if git:
2134 2136 buildopts['git'] = get('git')
2135 2137 if whitespace:
2136 2138 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2137 2139 buildopts['ignorewsamount'] = get('ignore_space_change',
2138 2140 'ignorewsamount')
2139 2141 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2140 2142 'ignoreblanklines')
2141 2143 if formatchanging:
2142 2144 buildopts['text'] = opts and opts.get('text')
2143 2145 buildopts['nobinary'] = get('nobinary')
2144 2146 buildopts['noprefix'] = get('noprefix', forceplain=False)
2145 2147
2146 2148 return mdiff.diffopts(**buildopts)
2147 2149
2148 2150 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2149 2151 losedatafn=None, prefix='', relroot=''):
2150 2152 '''yields diff of changes to files between two nodes, or node and
2151 2153 working directory.
2152 2154
2153 2155 if node1 is None, use first dirstate parent instead.
2154 2156 if node2 is None, compare node1 with working directory.
2155 2157
2156 2158 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2157 2159 every time some change cannot be represented with the current
2158 2160 patch format. Return False to upgrade to git patch format, True to
2159 2161 accept the loss or raise an exception to abort the diff. It is
2160 2162 called with the name of current file being diffed as 'fn'. If set
2161 2163 to None, patches will always be upgraded to git format when
2162 2164 necessary.
2163 2165
2164 2166 prefix is a filename prefix that is prepended to all filenames on
2165 2167 display (used for subrepos).
2166 2168
2167 2169 relroot, if not empty, must be normalized with a trailing /. Any match
2168 2170 patterns that fall outside it will be ignored.'''
2169 2171
2170 2172 if opts is None:
2171 2173 opts = mdiff.defaultopts
2172 2174
2173 2175 if not node1 and not node2:
2174 2176 node1 = repo.dirstate.p1()
2175 2177
2176 2178 def lrugetfilectx():
2177 2179 cache = {}
2178 2180 order = collections.deque()
2179 2181 def getfilectx(f, ctx):
2180 2182 fctx = ctx.filectx(f, filelog=cache.get(f))
2181 2183 if f not in cache:
2182 2184 if len(cache) > 20:
2183 2185 del cache[order.popleft()]
2184 2186 cache[f] = fctx.filelog()
2185 2187 else:
2186 2188 order.remove(f)
2187 2189 order.append(f)
2188 2190 return fctx
2189 2191 return getfilectx
2190 2192 getfilectx = lrugetfilectx()
2191 2193
2192 2194 ctx1 = repo[node1]
2193 2195 ctx2 = repo[node2]
2194 2196
2195 2197 relfiltered = False
2196 2198 if relroot != '' and match.always():
2197 2199 # as a special case, create a new matcher with just the relroot
2198 2200 pats = [relroot]
2199 2201 match = scmutil.match(ctx2, pats, default='path')
2200 2202 relfiltered = True
2201 2203
2202 2204 if not changes:
2203 2205 changes = repo.status(ctx1, ctx2, match=match)
2204 2206 modified, added, removed = changes[:3]
2205 2207
2206 2208 if not modified and not added and not removed:
2207 2209 return []
2208 2210
2209 2211 if repo.ui.debugflag:
2210 2212 hexfunc = hex
2211 2213 else:
2212 2214 hexfunc = short
2213 2215 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2214 2216
2215 2217 copy = {}
2216 2218 if opts.git or opts.upgrade:
2217 2219 copy = copies.pathcopies(ctx1, ctx2, match=match)
2218 2220
2219 2221 if relroot is not None:
2220 2222 if not relfiltered:
2221 2223 # XXX this would ideally be done in the matcher, but that is
2222 2224 # generally meant to 'or' patterns, not 'and' them. In this case we
2223 2225 # need to 'and' all the patterns from the matcher with relroot.
2224 2226 def filterrel(l):
2225 2227 return [f for f in l if f.startswith(relroot)]
2226 2228 modified = filterrel(modified)
2227 2229 added = filterrel(added)
2228 2230 removed = filterrel(removed)
2229 2231 relfiltered = True
2230 2232 # filter out copies where either side isn't inside the relative root
2231 2233 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2232 2234 if dst.startswith(relroot)
2233 2235 and src.startswith(relroot)))
2234 2236
2235 2237 def difffn(opts, losedata):
2236 2238 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2237 2239 copy, getfilectx, opts, losedata, prefix, relroot)
2238 2240 if opts.upgrade and not opts.git:
2239 2241 try:
2240 2242 def losedata(fn):
2241 2243 if not losedatafn or not losedatafn(fn=fn):
2242 2244 raise GitDiffRequired
2243 2245 # Buffer the whole output until we are sure it can be generated
2244 2246 return list(difffn(opts.copy(git=False), losedata))
2245 2247 except GitDiffRequired:
2246 2248 return difffn(opts.copy(git=True), None)
2247 2249 else:
2248 2250 return difffn(opts, None)
2249 2251
2250 2252 def difflabel(func, *args, **kw):
2251 2253 '''yields 2-tuples of (output, label) based on the output of func()'''
2252 2254 headprefixes = [('diff', 'diff.diffline'),
2253 2255 ('copy', 'diff.extended'),
2254 2256 ('rename', 'diff.extended'),
2255 2257 ('old', 'diff.extended'),
2256 2258 ('new', 'diff.extended'),
2257 2259 ('deleted', 'diff.extended'),
2258 2260 ('---', 'diff.file_a'),
2259 2261 ('+++', 'diff.file_b')]
2260 2262 textprefixes = [('@', 'diff.hunk'),
2261 2263 ('-', 'diff.deleted'),
2262 2264 ('+', 'diff.inserted')]
2263 2265 head = False
2264 2266 for chunk in func(*args, **kw):
2265 2267 lines = chunk.split('\n')
2266 2268 for i, line in enumerate(lines):
2267 2269 if i != 0:
2268 2270 yield ('\n', '')
2269 2271 if head:
2270 2272 if line.startswith('@'):
2271 2273 head = False
2272 2274 else:
2273 2275 if line and line[0] not in ' +-@\\':
2274 2276 head = True
2275 2277 stripline = line
2276 2278 diffline = False
2277 2279 if not head and line and line[0] in '+-':
2278 2280 # highlight tabs and trailing whitespace, but only in
2279 2281 # changed lines
2280 2282 stripline = line.rstrip()
2281 2283 diffline = True
2282 2284
2283 2285 prefixes = textprefixes
2284 2286 if head:
2285 2287 prefixes = headprefixes
2286 2288 for prefix, label in prefixes:
2287 2289 if stripline.startswith(prefix):
2288 2290 if diffline:
2289 2291 for token in tabsplitter.findall(stripline):
2290 2292 if '\t' == token[0]:
2291 2293 yield (token, 'diff.tab')
2292 2294 else:
2293 2295 yield (token, label)
2294 2296 else:
2295 2297 yield (stripline, label)
2296 2298 break
2297 2299 else:
2298 2300 yield (line, '')
2299 2301 if line != stripline:
2300 2302 yield (line[len(stripline):], 'diff.trailingwhitespace')
2301 2303
2302 2304 def diffui(*args, **kw):
2303 2305 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2304 2306 return difflabel(diff, *args, **kw)
2305 2307
2306 2308 def _filepairs(ctx1, modified, added, removed, copy, opts):
2307 2309 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2308 2310 before and f2 is the the name after. For added files, f1 will be None,
2309 2311 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2310 2312 or 'rename' (the latter two only if opts.git is set).'''
2311 2313 gone = set()
2312 2314
2313 2315 copyto = dict([(v, k) for k, v in copy.items()])
2314 2316
2315 2317 addedset, removedset = set(added), set(removed)
2316 2318 # Fix up added, since merged-in additions appear as
2317 2319 # modifications during merges
2318 2320 for f in modified:
2319 2321 if f not in ctx1:
2320 2322 addedset.add(f)
2321 2323
2322 2324 for f in sorted(modified + added + removed):
2323 2325 copyop = None
2324 2326 f1, f2 = f, f
2325 2327 if f in addedset:
2326 2328 f1 = None
2327 2329 if f in copy:
2328 2330 if opts.git:
2329 2331 f1 = copy[f]
2330 2332 if f1 in removedset and f1 not in gone:
2331 2333 copyop = 'rename'
2332 2334 gone.add(f1)
2333 2335 else:
2334 2336 copyop = 'copy'
2335 2337 elif f in removedset:
2336 2338 f2 = None
2337 2339 if opts.git:
2338 2340 # have we already reported a copy above?
2339 2341 if (f in copyto and copyto[f] in addedset
2340 2342 and copy[copyto[f]] == f):
2341 2343 continue
2342 2344 yield f1, f2, copyop
2343 2345
2344 2346 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2345 2347 copy, getfilectx, opts, losedatafn, prefix, relroot):
2346 2348 '''given input data, generate a diff and yield it in blocks
2347 2349
2348 2350 If generating a diff would lose data like flags or binary data and
2349 2351 losedatafn is not None, it will be called.
2350 2352
2351 2353 relroot is removed and prefix is added to every path in the diff output.
2352 2354
2353 2355 If relroot is not empty, this function expects every path in modified,
2354 2356 added, removed and copy to start with it.'''
2355 2357
2356 2358 def gitindex(text):
2357 2359 if not text:
2358 2360 text = ""
2359 2361 l = len(text)
2360 2362 s = util.sha1('blob %d\0' % l)
2361 2363 s.update(text)
2362 2364 return s.hexdigest()
2363 2365
2364 2366 if opts.noprefix:
2365 2367 aprefix = bprefix = ''
2366 2368 else:
2367 2369 aprefix = 'a/'
2368 2370 bprefix = 'b/'
2369 2371
2370 2372 def diffline(f, revs):
2371 2373 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2372 2374 return 'diff %s %s' % (revinfo, f)
2373 2375
2374 2376 date1 = util.datestr(ctx1.date())
2375 2377 date2 = util.datestr(ctx2.date())
2376 2378
2377 2379 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2378 2380
2379 2381 if relroot != '' and (repo.ui.configbool('devel', 'all')
2380 2382 or repo.ui.configbool('devel', 'check-relroot')):
2381 2383 for f in modified + added + removed + copy.keys() + copy.values():
2382 2384 if f is not None and not f.startswith(relroot):
2383 2385 raise AssertionError(
2384 2386 "file %s doesn't start with relroot %s" % (f, relroot))
2385 2387
2386 2388 for f1, f2, copyop in _filepairs(
2387 2389 ctx1, modified, added, removed, copy, opts):
2388 2390 content1 = None
2389 2391 content2 = None
2390 2392 flag1 = None
2391 2393 flag2 = None
2392 2394 if f1:
2393 2395 content1 = getfilectx(f1, ctx1).data()
2394 2396 if opts.git or losedatafn:
2395 2397 flag1 = ctx1.flags(f1)
2396 2398 if f2:
2397 2399 content2 = getfilectx(f2, ctx2).data()
2398 2400 if opts.git or losedatafn:
2399 2401 flag2 = ctx2.flags(f2)
2400 2402 binary = False
2401 2403 if opts.git or losedatafn:
2402 2404 binary = util.binary(content1) or util.binary(content2)
2403 2405
2404 2406 if losedatafn and not opts.git:
2405 2407 if (binary or
2406 2408 # copy/rename
2407 2409 f2 in copy or
2408 2410 # empty file creation
2409 2411 (not f1 and not content2) or
2410 2412 # empty file deletion
2411 2413 (not content1 and not f2) or
2412 2414 # create with flags
2413 2415 (not f1 and flag2) or
2414 2416 # change flags
2415 2417 (f1 and f2 and flag1 != flag2)):
2416 2418 losedatafn(f2 or f1)
2417 2419
2418 2420 path1 = f1 or f2
2419 2421 path2 = f2 or f1
2420 2422 path1 = posixpath.join(prefix, path1[len(relroot):])
2421 2423 path2 = posixpath.join(prefix, path2[len(relroot):])
2422 2424 header = []
2423 2425 if opts.git:
2424 2426 header.append('diff --git %s%s %s%s' %
2425 2427 (aprefix, path1, bprefix, path2))
2426 2428 if not f1: # added
2427 2429 header.append('new file mode %s' % gitmode[flag2])
2428 2430 elif not f2: # removed
2429 2431 header.append('deleted file mode %s' % gitmode[flag1])
2430 2432 else: # modified/copied/renamed
2431 2433 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2432 2434 if mode1 != mode2:
2433 2435 header.append('old mode %s' % mode1)
2434 2436 header.append('new mode %s' % mode2)
2435 2437 if copyop is not None:
2436 2438 header.append('%s from %s' % (copyop, path1))
2437 2439 header.append('%s to %s' % (copyop, path2))
2438 2440 elif revs and not repo.ui.quiet:
2439 2441 header.append(diffline(path1, revs))
2440 2442
2441 2443 if binary and opts.git and not opts.nobinary:
2442 2444 text = mdiff.b85diff(content1, content2)
2443 2445 if text:
2444 2446 header.append('index %s..%s' %
2445 2447 (gitindex(content1), gitindex(content2)))
2446 2448 else:
2447 2449 text = mdiff.unidiff(content1, date1,
2448 2450 content2, date2,
2449 2451 path1, path2, opts=opts)
2450 2452 if header and (text or len(header) > 1):
2451 2453 yield '\n'.join(header) + '\n'
2452 2454 if text:
2453 2455 yield text
2454 2456
2455 2457 def diffstatsum(stats):
2456 2458 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2457 2459 for f, a, r, b in stats:
2458 2460 maxfile = max(maxfile, encoding.colwidth(f))
2459 2461 maxtotal = max(maxtotal, a + r)
2460 2462 addtotal += a
2461 2463 removetotal += r
2462 2464 binary = binary or b
2463 2465
2464 2466 return maxfile, maxtotal, addtotal, removetotal, binary
2465 2467
2466 2468 def diffstatdata(lines):
2467 2469 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2468 2470
2469 2471 results = []
2470 2472 filename, adds, removes, isbinary = None, 0, 0, False
2471 2473
2472 2474 def addresult():
2473 2475 if filename:
2474 2476 results.append((filename, adds, removes, isbinary))
2475 2477
2476 2478 for line in lines:
2477 2479 if line.startswith('diff'):
2478 2480 addresult()
2479 2481 # set numbers to 0 anyway when starting new file
2480 2482 adds, removes, isbinary = 0, 0, False
2481 2483 if line.startswith('diff --git a/'):
2482 2484 filename = gitre.search(line).group(2)
2483 2485 elif line.startswith('diff -r'):
2484 2486 # format: "diff -r ... -r ... filename"
2485 2487 filename = diffre.search(line).group(1)
2486 2488 elif line.startswith('+') and not line.startswith('+++ '):
2487 2489 adds += 1
2488 2490 elif line.startswith('-') and not line.startswith('--- '):
2489 2491 removes += 1
2490 2492 elif (line.startswith('GIT binary patch') or
2491 2493 line.startswith('Binary file')):
2492 2494 isbinary = True
2493 2495 addresult()
2494 2496 return results
2495 2497
2496 2498 def diffstat(lines, width=80, git=False):
2497 2499 output = []
2498 2500 stats = diffstatdata(lines)
2499 2501 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2500 2502
2501 2503 countwidth = len(str(maxtotal))
2502 2504 if hasbinary and countwidth < 3:
2503 2505 countwidth = 3
2504 2506 graphwidth = width - countwidth - maxname - 6
2505 2507 if graphwidth < 10:
2506 2508 graphwidth = 10
2507 2509
2508 2510 def scale(i):
2509 2511 if maxtotal <= graphwidth:
2510 2512 return i
2511 2513 # If diffstat runs out of room it doesn't print anything,
2512 2514 # which isn't very useful, so always print at least one + or -
2513 2515 # if there were at least some changes.
2514 2516 return max(i * graphwidth // maxtotal, int(bool(i)))
2515 2517
2516 2518 for filename, adds, removes, isbinary in stats:
2517 2519 if isbinary:
2518 2520 count = 'Bin'
2519 2521 else:
2520 2522 count = adds + removes
2521 2523 pluses = '+' * scale(adds)
2522 2524 minuses = '-' * scale(removes)
2523 2525 output.append(' %s%s | %*s %s%s\n' %
2524 2526 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2525 2527 countwidth, count, pluses, minuses))
2526 2528
2527 2529 if stats:
2528 2530 output.append(_(' %d files changed, %d insertions(+), '
2529 2531 '%d deletions(-)\n')
2530 2532 % (len(stats), totaladds, totalremoves))
2531 2533
2532 2534 return ''.join(output)
2533 2535
2534 2536 def diffstatui(*args, **kw):
2535 2537 '''like diffstat(), but yields 2-tuples of (output, label) for
2536 2538 ui.write()
2537 2539 '''
2538 2540
2539 2541 for line in diffstat(*args, **kw).splitlines():
2540 2542 if line and line[-1] in '+-':
2541 2543 name, graph = line.rsplit(' ', 1)
2542 2544 yield (name + ' ', '')
2543 2545 m = re.search(r'\++', graph)
2544 2546 if m:
2545 2547 yield (m.group(0), 'diffstat.inserted')
2546 2548 m = re.search(r'-+', graph)
2547 2549 if m:
2548 2550 yield (m.group(0), 'diffstat.deleted')
2549 2551 else:
2550 2552 yield (line, '')
2551 2553 yield ('\n', '')
@@ -1,1501 +1,1528 b''
1 1 Set up a repo
2 2
3 3 $ cat <<EOF >> $HGRCPATH
4 4 > [ui]
5 5 > interactive = true
6 6 > [extensions]
7 7 > record =
8 8 > EOF
9 9
10 10 $ hg init a
11 11 $ cd a
12 12
13 13 Select no files
14 14
15 15 $ touch empty-rw
16 16 $ hg add empty-rw
17 17
18 18 $ hg commit -i empty-rw<<EOF
19 19 > n
20 20 > EOF
21 21 diff --git a/empty-rw b/empty-rw
22 22 new file mode 100644
23 23 examine changes to 'empty-rw'? [Ynesfdaq?] n
24 24
25 25 no changes to record
26 26
27 27 $ hg tip -p
28 28 changeset: -1:000000000000
29 29 tag: tip
30 30 user:
31 31 date: Thu Jan 01 00:00:00 1970 +0000
32 32
33 33
34 34
35 35 Select files but no hunks
36 36
37 37 $ hg commit -i empty-rw<<EOF
38 38 > y
39 39 > n
40 40 > EOF
41 41 diff --git a/empty-rw b/empty-rw
42 42 new file mode 100644
43 43 examine changes to 'empty-rw'? [Ynesfdaq?] y
44 44
45 45 abort: empty commit message
46 46 [255]
47 47
48 48 $ hg tip -p
49 49 changeset: -1:000000000000
50 50 tag: tip
51 51 user:
52 52 date: Thu Jan 01 00:00:00 1970 +0000
53 53
54 54
55 55
56 56 Record empty file
57 57
58 58 $ hg commit -i -d '0 0' -m empty empty-rw<<EOF
59 59 > y
60 60 > y
61 61 > EOF
62 62 diff --git a/empty-rw b/empty-rw
63 63 new file mode 100644
64 64 examine changes to 'empty-rw'? [Ynesfdaq?] y
65 65
66 66
67 67 $ hg tip -p
68 68 changeset: 0:c0708cf4e46e
69 69 tag: tip
70 70 user: test
71 71 date: Thu Jan 01 00:00:00 1970 +0000
72 72 summary: empty
73 73
74 74
75 75
76 76 Summary shows we updated to the new cset
77 77
78 78 $ hg summary
79 79 parent: 0:c0708cf4e46e tip
80 80 empty
81 81 branch: default
82 82 commit: (clean)
83 83 update: (current)
84 84 phases: 1 draft
85 85
86 86 Rename empty file
87 87
88 88 $ hg mv empty-rw empty-rename
89 89 $ hg commit -i -d '1 0' -m rename<<EOF
90 90 > y
91 91 > EOF
92 92 diff --git a/empty-rw b/empty-rename
93 93 rename from empty-rw
94 94 rename to empty-rename
95 95 examine changes to 'empty-rw' and 'empty-rename'? [Ynesfdaq?] y
96 96
97 97
98 98 $ hg tip -p
99 99 changeset: 1:d695e8dcb197
100 100 tag: tip
101 101 user: test
102 102 date: Thu Jan 01 00:00:01 1970 +0000
103 103 summary: rename
104 104
105 105
106 106
107 107 Copy empty file
108 108
109 109 $ hg cp empty-rename empty-copy
110 110 $ hg commit -i -d '2 0' -m copy<<EOF
111 111 > y
112 112 > EOF
113 113 diff --git a/empty-rename b/empty-copy
114 114 copy from empty-rename
115 115 copy to empty-copy
116 116 examine changes to 'empty-rename' and 'empty-copy'? [Ynesfdaq?] y
117 117
118 118
119 119 $ hg tip -p
120 120 changeset: 2:1d4b90bea524
121 121 tag: tip
122 122 user: test
123 123 date: Thu Jan 01 00:00:02 1970 +0000
124 124 summary: copy
125 125
126 126
127 127
128 128 Delete empty file
129 129
130 130 $ hg rm empty-copy
131 131 $ hg commit -i -d '3 0' -m delete<<EOF
132 132 > y
133 133 > EOF
134 134 diff --git a/empty-copy b/empty-copy
135 135 deleted file mode 100644
136 136 examine changes to 'empty-copy'? [Ynesfdaq?] y
137 137
138 138
139 139 $ hg tip -p
140 140 changeset: 3:b39a238f01a1
141 141 tag: tip
142 142 user: test
143 143 date: Thu Jan 01 00:00:03 1970 +0000
144 144 summary: delete
145 145
146 146
147 147
148 148 Add binary file
149 149
150 150 $ hg bundle --base -2 tip.bundle
151 151 1 changesets found
152 152 $ hg add tip.bundle
153 153 $ hg commit -i -d '4 0' -m binary<<EOF
154 154 > y
155 155 > EOF
156 156 diff --git a/tip.bundle b/tip.bundle
157 157 new file mode 100644
158 158 this is a binary file
159 159 examine changes to 'tip.bundle'? [Ynesfdaq?] y
160 160
161 161
162 162 $ hg tip -p
163 163 changeset: 4:ad816da3711e
164 164 tag: tip
165 165 user: test
166 166 date: Thu Jan 01 00:00:04 1970 +0000
167 167 summary: binary
168 168
169 169 diff -r b39a238f01a1 -r ad816da3711e tip.bundle
170 170 Binary file tip.bundle has changed
171 171
172 172
173 173 Change binary file
174 174
175 175 $ hg bundle --base -2 tip.bundle
176 176 1 changesets found
177 177 $ hg commit -i -d '5 0' -m binary-change<<EOF
178 178 > y
179 179 > EOF
180 180 diff --git a/tip.bundle b/tip.bundle
181 181 this modifies a binary file (all or nothing)
182 182 examine changes to 'tip.bundle'? [Ynesfdaq?] y
183 183
184 184
185 185 $ hg tip -p
186 186 changeset: 5:dccd6f3eb485
187 187 tag: tip
188 188 user: test
189 189 date: Thu Jan 01 00:00:05 1970 +0000
190 190 summary: binary-change
191 191
192 192 diff -r ad816da3711e -r dccd6f3eb485 tip.bundle
193 193 Binary file tip.bundle has changed
194 194
195 195
196 196 Rename and change binary file
197 197
198 198 $ hg mv tip.bundle top.bundle
199 199 $ hg bundle --base -2 top.bundle
200 200 1 changesets found
201 201 $ hg commit -i -d '6 0' -m binary-change-rename<<EOF
202 202 > y
203 203 > EOF
204 204 diff --git a/tip.bundle b/top.bundle
205 205 rename from tip.bundle
206 206 rename to top.bundle
207 207 this modifies a binary file (all or nothing)
208 208 examine changes to 'tip.bundle' and 'top.bundle'? [Ynesfdaq?] y
209 209
210 210
211 211 $ hg tip -p
212 212 changeset: 6:7fa44105f5b3
213 213 tag: tip
214 214 user: test
215 215 date: Thu Jan 01 00:00:06 1970 +0000
216 216 summary: binary-change-rename
217 217
218 218 diff -r dccd6f3eb485 -r 7fa44105f5b3 tip.bundle
219 219 Binary file tip.bundle has changed
220 220 diff -r dccd6f3eb485 -r 7fa44105f5b3 top.bundle
221 221 Binary file top.bundle has changed
222 222
223 223
224 224 Add plain file
225 225
226 226 $ for i in 1 2 3 4 5 6 7 8 9 10; do
227 227 > echo $i >> plain
228 228 > done
229 229
230 230 $ hg add plain
231 231 $ hg commit -i -d '7 0' -m plain plain<<EOF
232 232 > y
233 233 > y
234 234 > EOF
235 235 diff --git a/plain b/plain
236 236 new file mode 100644
237 237 examine changes to 'plain'? [Ynesfdaq?] y
238 238
239 239 @@ -0,0 +1,10 @@
240 240 +1
241 241 +2
242 242 +3
243 243 +4
244 244 +5
245 245 +6
246 246 +7
247 247 +8
248 248 +9
249 249 +10
250 250 record this change to 'plain'? [Ynesfdaq?] y
251 251
252 252 $ hg tip -p
253 253 changeset: 7:11fb457c1be4
254 254 tag: tip
255 255 user: test
256 256 date: Thu Jan 01 00:00:07 1970 +0000
257 257 summary: plain
258 258
259 259 diff -r 7fa44105f5b3 -r 11fb457c1be4 plain
260 260 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
261 261 +++ b/plain Thu Jan 01 00:00:07 1970 +0000
262 262 @@ -0,0 +1,10 @@
263 263 +1
264 264 +2
265 265 +3
266 266 +4
267 267 +5
268 268 +6
269 269 +7
270 270 +8
271 271 +9
272 272 +10
273 273
274 274 Modify end of plain file with username unset
275 275
276 276 $ echo 11 >> plain
277 277 $ unset HGUSER
278 278 $ hg commit -i --config ui.username= -d '8 0' -m end plain
279 279 abort: no username supplied
280 280 (use "hg config --edit" to set your username)
281 281 [255]
282 282
283 283
284 284 Modify end of plain file, also test that diffopts are accounted for
285 285
286 286 $ HGUSER="test"
287 287 $ export HGUSER
288 288 $ hg commit -i --config diff.showfunc=true -d '8 0' -m end plain <<EOF
289 289 > y
290 290 > y
291 291 > EOF
292 292 diff --git a/plain b/plain
293 293 1 hunks, 1 lines changed
294 294 examine changes to 'plain'? [Ynesfdaq?] y
295 295
296 296 @@ -8,3 +8,4 @@ 7
297 297 8
298 298 9
299 299 10
300 300 +11
301 301 record this change to 'plain'? [Ynesfdaq?] y
302 302
303 303
304 304 Modify end of plain file, no EOL
305 305
306 306 $ hg tip --template '{node}' >> plain
307 307 $ hg commit -i -d '9 0' -m noeol plain <<EOF
308 308 > y
309 309 > y
310 310 > EOF
311 311 diff --git a/plain b/plain
312 312 1 hunks, 1 lines changed
313 313 examine changes to 'plain'? [Ynesfdaq?] y
314 314
315 315 @@ -9,3 +9,4 @@
316 316 9
317 317 10
318 318 11
319 319 +7264f99c5f5ff3261504828afa4fb4d406c3af54
320 320 \ No newline at end of file
321 321 record this change to 'plain'? [Ynesfdaq?] y
322 322
323 323
324 324 Modify end of plain file, add EOL
325 325
326 326 $ echo >> plain
327 327 $ echo 1 > plain2
328 328 $ hg add plain2
329 329 $ hg commit -i -d '10 0' -m eol plain plain2 <<EOF
330 330 > y
331 331 > y
332 332 > y
333 333 > y
334 334 > EOF
335 335 diff --git a/plain b/plain
336 336 1 hunks, 1 lines changed
337 337 examine changes to 'plain'? [Ynesfdaq?] y
338 338
339 339 @@ -9,4 +9,4 @@
340 340 9
341 341 10
342 342 11
343 343 -7264f99c5f5ff3261504828afa4fb4d406c3af54
344 344 \ No newline at end of file
345 345 +7264f99c5f5ff3261504828afa4fb4d406c3af54
346 346 record change 1/2 to 'plain'? [Ynesfdaq?] y
347 347
348 348 diff --git a/plain2 b/plain2
349 349 new file mode 100644
350 350 examine changes to 'plain2'? [Ynesfdaq?] y
351 351
352 352 @@ -0,0 +1,1 @@
353 353 +1
354 354 record change 2/2 to 'plain2'? [Ynesfdaq?] y
355 355
356 356 Modify beginning, trim end, record both, add another file to test
357 357 changes numbering
358 358
359 359 $ rm plain
360 360 $ for i in 2 2 3 4 5 6 7 8 9 10; do
361 361 > echo $i >> plain
362 362 > done
363 363 $ echo 2 >> plain2
364 364
365 365 $ hg commit -i -d '10 0' -m begin-and-end plain plain2 <<EOF
366 366 > y
367 367 > y
368 368 > y
369 369 > y
370 370 > y
371 371 > EOF
372 372 diff --git a/plain b/plain
373 373 2 hunks, 3 lines changed
374 374 examine changes to 'plain'? [Ynesfdaq?] y
375 375
376 376 @@ -1,4 +1,4 @@
377 377 -1
378 378 +2
379 379 2
380 380 3
381 381 4
382 382 record change 1/3 to 'plain'? [Ynesfdaq?] y
383 383
384 384 @@ -8,5 +8,3 @@
385 385 8
386 386 9
387 387 10
388 388 -11
389 389 -7264f99c5f5ff3261504828afa4fb4d406c3af54
390 390 record change 2/3 to 'plain'? [Ynesfdaq?] y
391 391
392 392 diff --git a/plain2 b/plain2
393 393 1 hunks, 1 lines changed
394 394 examine changes to 'plain2'? [Ynesfdaq?] y
395 395
396 396 @@ -1,1 +1,2 @@
397 397 1
398 398 +2
399 399 record change 3/3 to 'plain2'? [Ynesfdaq?] y
400 400
401 401
402 402 $ hg tip -p
403 403 changeset: 11:21df83db12b8
404 404 tag: tip
405 405 user: test
406 406 date: Thu Jan 01 00:00:10 1970 +0000
407 407 summary: begin-and-end
408 408
409 409 diff -r ddb8b281c3ff -r 21df83db12b8 plain
410 410 --- a/plain Thu Jan 01 00:00:10 1970 +0000
411 411 +++ b/plain Thu Jan 01 00:00:10 1970 +0000
412 412 @@ -1,4 +1,4 @@
413 413 -1
414 414 +2
415 415 2
416 416 3
417 417 4
418 418 @@ -8,5 +8,3 @@
419 419 8
420 420 9
421 421 10
422 422 -11
423 423 -7264f99c5f5ff3261504828afa4fb4d406c3af54
424 424 diff -r ddb8b281c3ff -r 21df83db12b8 plain2
425 425 --- a/plain2 Thu Jan 01 00:00:10 1970 +0000
426 426 +++ b/plain2 Thu Jan 01 00:00:10 1970 +0000
427 427 @@ -1,1 +1,2 @@
428 428 1
429 429 +2
430 430
431 431
432 432 Trim beginning, modify end
433 433
434 434 $ rm plain
435 435 > for i in 4 5 6 7 8 9 10.new; do
436 436 > echo $i >> plain
437 437 > done
438 438
439 439 Record end
440 440
441 441 $ hg commit -i -d '11 0' -m end-only plain <<EOF
442 442 > y
443 443 > n
444 444 > y
445 445 > EOF
446 446 diff --git a/plain b/plain
447 447 2 hunks, 4 lines changed
448 448 examine changes to 'plain'? [Ynesfdaq?] y
449 449
450 450 @@ -1,9 +1,6 @@
451 451 -2
452 452 -2
453 453 -3
454 454 4
455 455 5
456 456 6
457 457 7
458 458 8
459 459 9
460 460 record change 1/2 to 'plain'? [Ynesfdaq?] n
461 461
462 462 @@ -4,7 +1,7 @@
463 463 4
464 464 5
465 465 6
466 466 7
467 467 8
468 468 9
469 469 -10
470 470 +10.new
471 471 record change 2/2 to 'plain'? [Ynesfdaq?] y
472 472
473 473
474 474 $ hg tip -p
475 475 changeset: 12:99337501826f
476 476 tag: tip
477 477 user: test
478 478 date: Thu Jan 01 00:00:11 1970 +0000
479 479 summary: end-only
480 480
481 481 diff -r 21df83db12b8 -r 99337501826f plain
482 482 --- a/plain Thu Jan 01 00:00:10 1970 +0000
483 483 +++ b/plain Thu Jan 01 00:00:11 1970 +0000
484 484 @@ -7,4 +7,4 @@
485 485 7
486 486 8
487 487 9
488 488 -10
489 489 +10.new
490 490
491 491
492 492 Record beginning
493 493
494 494 $ hg commit -i -d '12 0' -m begin-only plain <<EOF
495 495 > y
496 496 > y
497 497 > EOF
498 498 diff --git a/plain b/plain
499 499 1 hunks, 3 lines changed
500 500 examine changes to 'plain'? [Ynesfdaq?] y
501 501
502 502 @@ -1,6 +1,3 @@
503 503 -2
504 504 -2
505 505 -3
506 506 4
507 507 5
508 508 6
509 509 record this change to 'plain'? [Ynesfdaq?] y
510 510
511 511
512 512 $ hg tip -p
513 513 changeset: 13:bbd45465d540
514 514 tag: tip
515 515 user: test
516 516 date: Thu Jan 01 00:00:12 1970 +0000
517 517 summary: begin-only
518 518
519 519 diff -r 99337501826f -r bbd45465d540 plain
520 520 --- a/plain Thu Jan 01 00:00:11 1970 +0000
521 521 +++ b/plain Thu Jan 01 00:00:12 1970 +0000
522 522 @@ -1,6 +1,3 @@
523 523 -2
524 524 -2
525 525 -3
526 526 4
527 527 5
528 528 6
529 529
530 530
531 531 Add to beginning, trim from end
532 532
533 533 $ rm plain
534 534 $ for i in 1 2 3 4 5 6 7 8 9; do
535 535 > echo $i >> plain
536 536 > done
537 537
538 538 Record end
539 539
540 540 $ hg commit -i --traceback -d '13 0' -m end-again plain<<EOF
541 541 > y
542 542 > n
543 543 > y
544 544 > EOF
545 545 diff --git a/plain b/plain
546 546 2 hunks, 4 lines changed
547 547 examine changes to 'plain'? [Ynesfdaq?] y
548 548
549 549 @@ -1,6 +1,9 @@
550 550 +1
551 551 +2
552 552 +3
553 553 4
554 554 5
555 555 6
556 556 7
557 557 8
558 558 9
559 559 record change 1/2 to 'plain'? [Ynesfdaq?] n
560 560
561 561 @@ -1,7 +4,6 @@
562 562 4
563 563 5
564 564 6
565 565 7
566 566 8
567 567 9
568 568 -10.new
569 569 record change 2/2 to 'plain'? [Ynesfdaq?] y
570 570
571 571
572 572 Add to beginning, middle, end
573 573
574 574 $ rm plain
575 575 $ for i in 1 2 3 4 5 5.new 5.reallynew 6 7 8 9 10 11; do
576 576 > echo $i >> plain
577 577 > done
578 578
579 579 Record beginning, middle, and test that format-breaking diffopts are ignored
580 580
581 581 $ hg commit -i --config diff.noprefix=True -d '14 0' -m middle-only plain <<EOF
582 582 > y
583 583 > y
584 584 > y
585 585 > n
586 586 > EOF
587 587 diff --git a/plain b/plain
588 588 3 hunks, 7 lines changed
589 589 examine changes to 'plain'? [Ynesfdaq?] y
590 590
591 591 @@ -1,2 +1,5 @@
592 592 +1
593 593 +2
594 594 +3
595 595 4
596 596 5
597 597 record change 1/3 to 'plain'? [Ynesfdaq?] y
598 598
599 599 @@ -1,6 +4,8 @@
600 600 4
601 601 5
602 602 +5.new
603 603 +5.reallynew
604 604 6
605 605 7
606 606 8
607 607 9
608 608 record change 2/3 to 'plain'? [Ynesfdaq?] y
609 609
610 610 @@ -3,4 +8,6 @@
611 611 6
612 612 7
613 613 8
614 614 9
615 615 +10
616 616 +11
617 617 record change 3/3 to 'plain'? [Ynesfdaq?] n
618 618
619 619
620 620 $ hg tip -p
621 621 changeset: 15:f34a7937ec33
622 622 tag: tip
623 623 user: test
624 624 date: Thu Jan 01 00:00:14 1970 +0000
625 625 summary: middle-only
626 626
627 627 diff -r 82c065d0b850 -r f34a7937ec33 plain
628 628 --- a/plain Thu Jan 01 00:00:13 1970 +0000
629 629 +++ b/plain Thu Jan 01 00:00:14 1970 +0000
630 630 @@ -1,5 +1,10 @@
631 631 +1
632 632 +2
633 633 +3
634 634 4
635 635 5
636 636 +5.new
637 637 +5.reallynew
638 638 6
639 639 7
640 640 8
641 641
642 642
643 643 Record end
644 644
645 645 $ hg commit -i -d '15 0' -m end-only plain <<EOF
646 646 > y
647 647 > y
648 648 > EOF
649 649 diff --git a/plain b/plain
650 650 1 hunks, 2 lines changed
651 651 examine changes to 'plain'? [Ynesfdaq?] y
652 652
653 653 @@ -9,3 +9,5 @@
654 654 7
655 655 8
656 656 9
657 657 +10
658 658 +11
659 659 record this change to 'plain'? [Ynesfdaq?] y
660 660
661 661
662 662 $ hg tip -p
663 663 changeset: 16:f9900b71a04c
664 664 tag: tip
665 665 user: test
666 666 date: Thu Jan 01 00:00:15 1970 +0000
667 667 summary: end-only
668 668
669 669 diff -r f34a7937ec33 -r f9900b71a04c plain
670 670 --- a/plain Thu Jan 01 00:00:14 1970 +0000
671 671 +++ b/plain Thu Jan 01 00:00:15 1970 +0000
672 672 @@ -9,3 +9,5 @@
673 673 7
674 674 8
675 675 9
676 676 +10
677 677 +11
678 678
679 679
680 680 $ mkdir subdir
681 681 $ cd subdir
682 682 $ echo a > a
683 683 $ hg ci -d '16 0' -Amsubdir
684 684 adding subdir/a
685 685
686 686 $ echo a >> a
687 687 $ hg commit -i -d '16 0' -m subdir-change a <<EOF
688 688 > y
689 689 > y
690 690 > EOF
691 691 diff --git a/subdir/a b/subdir/a
692 692 1 hunks, 1 lines changed
693 693 examine changes to 'subdir/a'? [Ynesfdaq?] y
694 694
695 695 @@ -1,1 +1,2 @@
696 696 a
697 697 +a
698 698 record this change to 'subdir/a'? [Ynesfdaq?] y
699 699
700 700
701 701 $ hg tip -p
702 702 changeset: 18:61be427a9deb
703 703 tag: tip
704 704 user: test
705 705 date: Thu Jan 01 00:00:16 1970 +0000
706 706 summary: subdir-change
707 707
708 708 diff -r a7ffae4d61cb -r 61be427a9deb subdir/a
709 709 --- a/subdir/a Thu Jan 01 00:00:16 1970 +0000
710 710 +++ b/subdir/a Thu Jan 01 00:00:16 1970 +0000
711 711 @@ -1,1 +1,2 @@
712 712 a
713 713 +a
714 714
715 715
716 716 $ echo a > f1
717 717 $ echo b > f2
718 718 $ hg add f1 f2
719 719
720 720 $ hg ci -mz -d '17 0'
721 721
722 722 $ echo a >> f1
723 723 $ echo b >> f2
724 724
725 725 Help, quit
726 726
727 727 $ hg commit -i <<EOF
728 728 > ?
729 729 > q
730 730 > EOF
731 731 diff --git a/subdir/f1 b/subdir/f1
732 732 1 hunks, 1 lines changed
733 733 examine changes to 'subdir/f1'? [Ynesfdaq?] ?
734 734
735 735 y - yes, record this change
736 736 n - no, skip this change
737 737 e - edit this change manually
738 738 s - skip remaining changes to this file
739 739 f - record remaining changes to this file
740 740 d - done, skip remaining changes and files
741 741 a - record all changes to all remaining files
742 742 q - quit, recording no changes
743 743 ? - ? (display help)
744 744 examine changes to 'subdir/f1'? [Ynesfdaq?] q
745 745
746 746 abort: user quit
747 747 [255]
748 748
749 749 Skip
750 750
751 751 $ hg commit -i <<EOF
752 752 > s
753 753 > EOF
754 754 diff --git a/subdir/f1 b/subdir/f1
755 755 1 hunks, 1 lines changed
756 756 examine changes to 'subdir/f1'? [Ynesfdaq?] s
757 757
758 758 diff --git a/subdir/f2 b/subdir/f2
759 759 1 hunks, 1 lines changed
760 760 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
761 761 [255]
762 762
763 763 No
764 764
765 765 $ hg commit -i <<EOF
766 766 > n
767 767 > EOF
768 768 diff --git a/subdir/f1 b/subdir/f1
769 769 1 hunks, 1 lines changed
770 770 examine changes to 'subdir/f1'? [Ynesfdaq?] n
771 771
772 772 diff --git a/subdir/f2 b/subdir/f2
773 773 1 hunks, 1 lines changed
774 774 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
775 775 [255]
776 776
777 777 f, quit
778 778
779 779 $ hg commit -i <<EOF
780 780 > f
781 781 > q
782 782 > EOF
783 783 diff --git a/subdir/f1 b/subdir/f1
784 784 1 hunks, 1 lines changed
785 785 examine changes to 'subdir/f1'? [Ynesfdaq?] f
786 786
787 787 diff --git a/subdir/f2 b/subdir/f2
788 788 1 hunks, 1 lines changed
789 789 examine changes to 'subdir/f2'? [Ynesfdaq?] q
790 790
791 791 abort: user quit
792 792 [255]
793 793
794 794 s, all
795 795
796 796 $ hg commit -i -d '18 0' -mx <<EOF
797 797 > s
798 798 > a
799 799 > EOF
800 800 diff --git a/subdir/f1 b/subdir/f1
801 801 1 hunks, 1 lines changed
802 802 examine changes to 'subdir/f1'? [Ynesfdaq?] s
803 803
804 804 diff --git a/subdir/f2 b/subdir/f2
805 805 1 hunks, 1 lines changed
806 806 examine changes to 'subdir/f2'? [Ynesfdaq?] a
807 807
808 808
809 809 $ hg tip -p
810 810 changeset: 20:b3df3dda369a
811 811 tag: tip
812 812 user: test
813 813 date: Thu Jan 01 00:00:18 1970 +0000
814 814 summary: x
815 815
816 816 diff -r 6e02d6c9906d -r b3df3dda369a subdir/f2
817 817 --- a/subdir/f2 Thu Jan 01 00:00:17 1970 +0000
818 818 +++ b/subdir/f2 Thu Jan 01 00:00:18 1970 +0000
819 819 @@ -1,1 +1,2 @@
820 820 b
821 821 +b
822 822
823 823
824 824 f
825 825
826 826 $ hg commit -i -d '19 0' -my <<EOF
827 827 > f
828 828 > EOF
829 829 diff --git a/subdir/f1 b/subdir/f1
830 830 1 hunks, 1 lines changed
831 831 examine changes to 'subdir/f1'? [Ynesfdaq?] f
832 832
833 833
834 834 $ hg tip -p
835 835 changeset: 21:38ec577f126b
836 836 tag: tip
837 837 user: test
838 838 date: Thu Jan 01 00:00:19 1970 +0000
839 839 summary: y
840 840
841 841 diff -r b3df3dda369a -r 38ec577f126b subdir/f1
842 842 --- a/subdir/f1 Thu Jan 01 00:00:18 1970 +0000
843 843 +++ b/subdir/f1 Thu Jan 01 00:00:19 1970 +0000
844 844 @@ -1,1 +1,2 @@
845 845 a
846 846 +a
847 847
848 848
849 849 #if execbit
850 850
851 851 Preserve chmod +x
852 852
853 853 $ chmod +x f1
854 854 $ echo a >> f1
855 855 $ hg commit -i -d '20 0' -mz <<EOF
856 856 > y
857 857 > y
858 858 > y
859 859 > EOF
860 860 diff --git a/subdir/f1 b/subdir/f1
861 861 old mode 100644
862 862 new mode 100755
863 863 1 hunks, 1 lines changed
864 864 examine changes to 'subdir/f1'? [Ynesfdaq?] y
865 865
866 866 @@ -1,2 +1,3 @@
867 867 a
868 868 a
869 869 +a
870 870 record this change to 'subdir/f1'? [Ynesfdaq?] y
871 871
872 872
873 873 $ hg tip --config diff.git=True -p
874 874 changeset: 22:3261adceb075
875 875 tag: tip
876 876 user: test
877 877 date: Thu Jan 01 00:00:20 1970 +0000
878 878 summary: z
879 879
880 880 diff --git a/subdir/f1 b/subdir/f1
881 881 old mode 100644
882 882 new mode 100755
883 883 --- a/subdir/f1
884 884 +++ b/subdir/f1
885 885 @@ -1,2 +1,3 @@
886 886 a
887 887 a
888 888 +a
889 889
890 890
891 891 Preserve execute permission on original
892 892
893 893 $ echo b >> f1
894 894 $ hg commit -i -d '21 0' -maa <<EOF
895 895 > y
896 896 > y
897 897 > y
898 898 > EOF
899 899 diff --git a/subdir/f1 b/subdir/f1
900 900 1 hunks, 1 lines changed
901 901 examine changes to 'subdir/f1'? [Ynesfdaq?] y
902 902
903 903 @@ -1,3 +1,4 @@
904 904 a
905 905 a
906 906 a
907 907 +b
908 908 record this change to 'subdir/f1'? [Ynesfdaq?] y
909 909
910 910
911 911 $ hg tip --config diff.git=True -p
912 912 changeset: 23:b429867550db
913 913 tag: tip
914 914 user: test
915 915 date: Thu Jan 01 00:00:21 1970 +0000
916 916 summary: aa
917 917
918 918 diff --git a/subdir/f1 b/subdir/f1
919 919 --- a/subdir/f1
920 920 +++ b/subdir/f1
921 921 @@ -1,3 +1,4 @@
922 922 a
923 923 a
924 924 a
925 925 +b
926 926
927 927
928 928 Preserve chmod -x
929 929
930 930 $ chmod -x f1
931 931 $ echo c >> f1
932 932 $ hg commit -i -d '22 0' -mab <<EOF
933 933 > y
934 934 > y
935 935 > y
936 936 > EOF
937 937 diff --git a/subdir/f1 b/subdir/f1
938 938 old mode 100755
939 939 new mode 100644
940 940 1 hunks, 1 lines changed
941 941 examine changes to 'subdir/f1'? [Ynesfdaq?] y
942 942
943 943 @@ -2,3 +2,4 @@
944 944 a
945 945 a
946 946 b
947 947 +c
948 948 record this change to 'subdir/f1'? [Ynesfdaq?] y
949 949
950 950
951 951 $ hg tip --config diff.git=True -p
952 952 changeset: 24:0b082130c20a
953 953 tag: tip
954 954 user: test
955 955 date: Thu Jan 01 00:00:22 1970 +0000
956 956 summary: ab
957 957
958 958 diff --git a/subdir/f1 b/subdir/f1
959 959 old mode 100755
960 960 new mode 100644
961 961 --- a/subdir/f1
962 962 +++ b/subdir/f1
963 963 @@ -2,3 +2,4 @@
964 964 a
965 965 a
966 966 b
967 967 +c
968 968
969 969
970 970 #else
971 971
972 972 Slightly bogus tests to get almost same repo structure as when x bit is used
973 973 - but with different hashes.
974 974
975 975 Mock "Preserve chmod +x"
976 976
977 977 $ echo a >> f1
978 978 $ hg commit -i -d '20 0' -mz <<EOF
979 979 > y
980 980 > y
981 981 > y
982 982 > EOF
983 983 diff --git a/subdir/f1 b/subdir/f1
984 984 1 hunks, 1 lines changed
985 985 examine changes to 'subdir/f1'? [Ynesfdaq?] y
986 986
987 987 @@ -1,2 +1,3 @@
988 988 a
989 989 a
990 990 +a
991 991 record this change to 'subdir/f1'? [Ynesfdaq?] y
992 992
993 993
994 994 $ hg tip --config diff.git=True -p
995 995 changeset: 22:0d463bd428f5
996 996 tag: tip
997 997 user: test
998 998 date: Thu Jan 01 00:00:20 1970 +0000
999 999 summary: z
1000 1000
1001 1001 diff --git a/subdir/f1 b/subdir/f1
1002 1002 --- a/subdir/f1
1003 1003 +++ b/subdir/f1
1004 1004 @@ -1,2 +1,3 @@
1005 1005 a
1006 1006 a
1007 1007 +a
1008 1008
1009 1009
1010 1010 Mock "Preserve execute permission on original"
1011 1011
1012 1012 $ echo b >> f1
1013 1013 $ hg commit -i -d '21 0' -maa <<EOF
1014 1014 > y
1015 1015 > y
1016 1016 > y
1017 1017 > EOF
1018 1018 diff --git a/subdir/f1 b/subdir/f1
1019 1019 1 hunks, 1 lines changed
1020 1020 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1021 1021
1022 1022 @@ -1,3 +1,4 @@
1023 1023 a
1024 1024 a
1025 1025 a
1026 1026 +b
1027 1027 record this change to 'subdir/f1'? [Ynesfdaq?] y
1028 1028
1029 1029
1030 1030 $ hg tip --config diff.git=True -p
1031 1031 changeset: 23:0eab41a3e524
1032 1032 tag: tip
1033 1033 user: test
1034 1034 date: Thu Jan 01 00:00:21 1970 +0000
1035 1035 summary: aa
1036 1036
1037 1037 diff --git a/subdir/f1 b/subdir/f1
1038 1038 --- a/subdir/f1
1039 1039 +++ b/subdir/f1
1040 1040 @@ -1,3 +1,4 @@
1041 1041 a
1042 1042 a
1043 1043 a
1044 1044 +b
1045 1045
1046 1046
1047 1047 Mock "Preserve chmod -x"
1048 1048
1049 1049 $ chmod -x f1
1050 1050 $ echo c >> f1
1051 1051 $ hg commit -i -d '22 0' -mab <<EOF
1052 1052 > y
1053 1053 > y
1054 1054 > y
1055 1055 > EOF
1056 1056 diff --git a/subdir/f1 b/subdir/f1
1057 1057 1 hunks, 1 lines changed
1058 1058 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1059 1059
1060 1060 @@ -2,3 +2,4 @@
1061 1061 a
1062 1062 a
1063 1063 b
1064 1064 +c
1065 1065 record this change to 'subdir/f1'? [Ynesfdaq?] y
1066 1066
1067 1067
1068 1068 $ hg tip --config diff.git=True -p
1069 1069 changeset: 24:f4f718f27b7c
1070 1070 tag: tip
1071 1071 user: test
1072 1072 date: Thu Jan 01 00:00:22 1970 +0000
1073 1073 summary: ab
1074 1074
1075 1075 diff --git a/subdir/f1 b/subdir/f1
1076 1076 --- a/subdir/f1
1077 1077 +++ b/subdir/f1
1078 1078 @@ -2,3 +2,4 @@
1079 1079 a
1080 1080 a
1081 1081 b
1082 1082 +c
1083 1083
1084 1084
1085 1085 #endif
1086 1086
1087 1087 $ cd ..
1088 1088
1089 1089
1090 1090 Abort early when a merge is in progress
1091 1091
1092 1092 $ hg up 4
1093 1093 1 files updated, 0 files merged, 6 files removed, 0 files unresolved
1094 1094
1095 1095 $ touch iwillmergethat
1096 1096 $ hg add iwillmergethat
1097 1097
1098 1098 $ hg branch thatbranch
1099 1099 marked working directory as branch thatbranch
1100 1100 (branches are permanent and global, did you want a bookmark?)
1101 1101
1102 1102 $ hg ci -m'new head'
1103 1103
1104 1104 $ hg up default
1105 1105 6 files updated, 0 files merged, 2 files removed, 0 files unresolved
1106 1106
1107 1107 $ hg merge thatbranch
1108 1108 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1109 1109 (branch merge, don't forget to commit)
1110 1110
1111 1111 $ hg commit -i -m'will abort'
1112 1112 abort: cannot partially commit a merge (use "hg commit" instead)
1113 1113 [255]
1114 1114
1115 1115 $ hg up -C
1116 1116 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1117 1117
1118 1118 Editing patch (and ignoring trailing text)
1119 1119
1120 1120 $ cat > editor.sh << '__EOF__'
1121 1121 > sed -e 7d -e '5s/^-/ /' -e '/^# ---/i\
1122 1122 > trailing\nditto' "$1" > tmp
1123 1123 > mv tmp "$1"
1124 1124 > __EOF__
1125 1125 $ cat > editedfile << '__EOF__'
1126 1126 > This is the first line
1127 1127 > This is the second line
1128 1128 > This is the third line
1129 1129 > __EOF__
1130 1130 $ hg add editedfile
1131 1131 $ hg commit -medit-patch-1
1132 1132 $ cat > editedfile << '__EOF__'
1133 1133 > This line has changed
1134 1134 > This change will be committed
1135 1135 > This is the third line
1136 1136 > __EOF__
1137 1137 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-2 <<EOF
1138 1138 > y
1139 1139 > e
1140 1140 > EOF
1141 1141 diff --git a/editedfile b/editedfile
1142 1142 1 hunks, 2 lines changed
1143 1143 examine changes to 'editedfile'? [Ynesfdaq?] y
1144 1144
1145 1145 @@ -1,3 +1,3 @@
1146 1146 -This is the first line
1147 1147 -This is the second line
1148 1148 +This line has changed
1149 1149 +This change will be committed
1150 1150 This is the third line
1151 1151 record this change to 'editedfile'? [Ynesfdaq?] e
1152 1152
1153 1153 $ cat editedfile
1154 1154 This line has changed
1155 1155 This change will be committed
1156 1156 This is the third line
1157 1157 $ hg cat -r tip editedfile
1158 1158 This is the first line
1159 1159 This change will be committed
1160 1160 This is the third line
1161 1161 $ hg revert editedfile
1162 1162
1163 1163 Trying to edit patch for whole file
1164 1164
1165 1165 $ echo "This is the fourth line" >> editedfile
1166 1166 $ hg commit -i <<EOF
1167 1167 > e
1168 1168 > q
1169 1169 > EOF
1170 1170 diff --git a/editedfile b/editedfile
1171 1171 1 hunks, 1 lines changed
1172 1172 examine changes to 'editedfile'? [Ynesfdaq?] e
1173 1173
1174 1174 cannot edit patch for whole file
1175 1175 examine changes to 'editedfile'? [Ynesfdaq?] q
1176 1176
1177 1177 abort: user quit
1178 1178 [255]
1179 1179 $ hg revert editedfile
1180 1180
1181 1181 Removing changes from patch
1182 1182
1183 1183 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1184 1184 $ mv tmp editedfile
1185 1185 $ echo "This line has been added" >> editedfile
1186 1186 $ cat > editor.sh << '__EOF__'
1187 1187 > sed -e 's/^[-+]/ /' "$1" > tmp
1188 1188 > mv tmp "$1"
1189 1189 > __EOF__
1190 1190 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1191 1191 > y
1192 1192 > e
1193 1193 > EOF
1194 1194 diff --git a/editedfile b/editedfile
1195 1195 1 hunks, 3 lines changed
1196 1196 examine changes to 'editedfile'? [Ynesfdaq?] y
1197 1197
1198 1198 @@ -1,3 +1,3 @@
1199 1199 -This is the first line
1200 1200 -This change will be committed
1201 1201 -This is the third line
1202 1202 +This change will not be committed
1203 1203 +This is the second line
1204 1204 +This line has been added
1205 1205 record this change to 'editedfile'? [Ynesfdaq?] e
1206 1206
1207 1207 no changes to record
1208 1208 $ cat editedfile
1209 1209 This change will not be committed
1210 1210 This is the second line
1211 1211 This line has been added
1212 1212 $ hg cat -r tip editedfile
1213 1213 This is the first line
1214 1214 This change will be committed
1215 1215 This is the third line
1216 1216 $ hg revert editedfile
1217 1217
1218 1218 Invalid patch
1219 1219
1220 1220 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1221 1221 $ mv tmp editedfile
1222 1222 $ echo "This line has been added" >> editedfile
1223 1223 $ cat > editor.sh << '__EOF__'
1224 1224 > sed s/This/That/ "$1" > tmp
1225 1225 > mv tmp "$1"
1226 1226 > __EOF__
1227 1227 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1228 1228 > y
1229 1229 > e
1230 1230 > EOF
1231 1231 diff --git a/editedfile b/editedfile
1232 1232 1 hunks, 3 lines changed
1233 1233 examine changes to 'editedfile'? [Ynesfdaq?] y
1234 1234
1235 1235 @@ -1,3 +1,3 @@
1236 1236 -This is the first line
1237 1237 -This change will be committed
1238 1238 -This is the third line
1239 1239 +This change will not be committed
1240 1240 +This is the second line
1241 1241 +This line has been added
1242 1242 record this change to 'editedfile'? [Ynesfdaq?] e
1243 1243
1244 1244 patching file editedfile
1245 1245 Hunk #1 FAILED at 0
1246 1246 1 out of 1 hunks FAILED -- saving rejects to file editedfile.rej
1247 1247 abort: patch failed to apply
1248 1248 [255]
1249 1249 $ cat editedfile
1250 1250 This change will not be committed
1251 1251 This is the second line
1252 1252 This line has been added
1253 1253 $ hg cat -r tip editedfile
1254 1254 This is the first line
1255 1255 This change will be committed
1256 1256 This is the third line
1257 1257 $ cat editedfile.rej
1258 1258 --- editedfile
1259 1259 +++ editedfile
1260 1260 @@ -1,3 +1,3 @@
1261 1261 -That is the first line
1262 1262 -That change will be committed
1263 1263 -That is the third line
1264 1264 +That change will not be committed
1265 1265 +That is the second line
1266 1266 +That line has been added
1267 1267
1268 1268 Malformed patch - error handling
1269 1269
1270 1270 $ cat > editor.sh << '__EOF__'
1271 1271 > sed -e '/^@/p' "$1" > tmp
1272 1272 > mv tmp "$1"
1273 1273 > __EOF__
1274 1274 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1275 1275 > y
1276 1276 > e
1277 1277 > EOF
1278 1278 diff --git a/editedfile b/editedfile
1279 1279 1 hunks, 3 lines changed
1280 1280 examine changes to 'editedfile'? [Ynesfdaq?] y
1281 1281
1282 1282 @@ -1,3 +1,3 @@
1283 1283 -This is the first line
1284 1284 -This change will be committed
1285 1285 -This is the third line
1286 1286 +This change will not be committed
1287 1287 +This is the second line
1288 1288 +This line has been added
1289 1289 record this change to 'editedfile'? [Ynesfdaq?] e
1290 1290
1291 1291 abort: error parsing patch: unhandled transition: range -> range
1292 1292 [255]
1293 1293
1294 Exiting editor with status 1, ignores the edit but does not stop the recording
1295 session
1296
1297 $ HGEDITOR=false hg commit -i <<EOF
1298 > y
1299 > e
1300 > n
1301 > EOF
1302 diff --git a/editedfile b/editedfile
1303 1 hunks, 3 lines changed
1304 examine changes to 'editedfile'? [Ynesfdaq?] y
1305
1306 @@ -1,3 +1,3 @@
1307 -This is the first line
1308 -This change will be committed
1309 -This is the third line
1310 +This change will not be committed
1311 +This is the second line
1312 +This line has been added
1313 record this change to 'editedfile'? [Ynesfdaq?] e
1314
1315 editor exited with exit code 1
1316 record this change to 'editedfile'? [Ynesfdaq?] n
1317
1318 no changes to record
1319
1320
1294 1321 random text in random positions is still an error
1295 1322
1296 1323 $ cat > editor.sh << '__EOF__'
1297 1324 > sed -e '/^@/i\
1298 1325 > other' "$1" > tmp
1299 1326 > mv tmp "$1"
1300 1327 > __EOF__
1301 1328 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1302 1329 > y
1303 1330 > e
1304 1331 > EOF
1305 1332 diff --git a/editedfile b/editedfile
1306 1333 1 hunks, 3 lines changed
1307 1334 examine changes to 'editedfile'? [Ynesfdaq?] y
1308 1335
1309 1336 @@ -1,3 +1,3 @@
1310 1337 -This is the first line
1311 1338 -This change will be committed
1312 1339 -This is the third line
1313 1340 +This change will not be committed
1314 1341 +This is the second line
1315 1342 +This line has been added
1316 1343 record this change to 'editedfile'? [Ynesfdaq?] e
1317 1344
1318 1345 abort: error parsing patch: unhandled transition: file -> other
1319 1346 [255]
1320 1347
1321 1348 $ hg up -C
1322 1349 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1323 1350
1324 1351 With win32text
1325 1352
1326 1353 $ echo '[extensions]' >> .hg/hgrc
1327 1354 $ echo 'win32text = ' >> .hg/hgrc
1328 1355 $ echo '[decode]' >> .hg/hgrc
1329 1356 $ echo '** = cleverdecode:' >> .hg/hgrc
1330 1357 $ echo '[encode]' >> .hg/hgrc
1331 1358 $ echo '** = cleverencode:' >> .hg/hgrc
1332 1359 $ echo '[patch]' >> .hg/hgrc
1333 1360 $ echo 'eol = crlf' >> .hg/hgrc
1334 1361
1335 1362 Ignore win32text deprecation warning for now:
1336 1363
1337 1364 $ echo '[win32text]' >> .hg/hgrc
1338 1365 $ echo 'warn = no' >> .hg/hgrc
1339 1366
1340 1367 $ echo d >> subdir/f1
1341 1368 $ hg commit -i -d '24 0' -mw1 <<EOF
1342 1369 > y
1343 1370 > y
1344 1371 > EOF
1345 1372 diff --git a/subdir/f1 b/subdir/f1
1346 1373 1 hunks, 1 lines changed
1347 1374 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1348 1375
1349 1376 @@ -3,3 +3,4 @@
1350 1377 a
1351 1378 b
1352 1379 c
1353 1380 +d
1354 1381 record this change to 'subdir/f1'? [Ynesfdaq?] y
1355 1382
1356 1383
1357 1384 $ hg tip -p
1358 1385 changeset: 28:* (glob)
1359 1386 tag: tip
1360 1387 user: test
1361 1388 date: Thu Jan 01 00:00:24 1970 +0000
1362 1389 summary: w1
1363 1390
1364 1391 diff -r ???????????? -r ???????????? subdir/f1 (glob)
1365 1392 --- a/subdir/f1 Thu Jan 01 00:00:23 1970 +0000
1366 1393 +++ b/subdir/f1 Thu Jan 01 00:00:24 1970 +0000
1367 1394 @@ -3,3 +3,4 @@
1368 1395 a
1369 1396 b
1370 1397 c
1371 1398 +d
1372 1399
1373 1400
1374 1401
1375 1402 Test --user when ui.username not set
1376 1403 $ unset HGUSER
1377 1404 $ echo e >> subdir/f1
1378 1405 $ hg commit -i --config ui.username= -d '8 0' --user xyz -m "user flag" <<EOF
1379 1406 > y
1380 1407 > y
1381 1408 > EOF
1382 1409 diff --git a/subdir/f1 b/subdir/f1
1383 1410 1 hunks, 1 lines changed
1384 1411 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1385 1412
1386 1413 @@ -4,3 +4,4 @@
1387 1414 b
1388 1415 c
1389 1416 d
1390 1417 +e
1391 1418 record this change to 'subdir/f1'? [Ynesfdaq?] y
1392 1419
1393 1420 $ hg log --template '{author}\n' -l 1
1394 1421 xyz
1395 1422 $ HGUSER="test"
1396 1423 $ export HGUSER
1397 1424
1398 1425
1399 1426 Moving files
1400 1427
1401 1428 $ hg update -C .
1402 1429 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1403 1430 $ hg mv plain plain3
1404 1431 $ echo somechange >> plain3
1405 1432 $ hg commit -i -d '23 0' -mmoving_files << EOF
1406 1433 > y
1407 1434 > y
1408 1435 > EOF
1409 1436 diff --git a/plain b/plain3
1410 1437 rename from plain
1411 1438 rename to plain3
1412 1439 1 hunks, 1 lines changed
1413 1440 examine changes to 'plain' and 'plain3'? [Ynesfdaq?] y
1414 1441
1415 1442 @@ -11,3 +11,4 @@
1416 1443 9
1417 1444 10
1418 1445 11
1419 1446 +somechange
1420 1447 record this change to 'plain3'? [Ynesfdaq?] y
1421 1448
1422 1449 The #if execbit block above changes the hash here on some systems
1423 1450 $ hg tip
1424 1451 changeset: 30:* (glob)
1425 1452 tag: tip
1426 1453 user: test
1427 1454 date: Thu Jan 01 00:00:23 1970 +0000
1428 1455 summary: moving_files
1429 1456
1430 1457 Editing patch of newly added file
1431 1458
1432 1459 $ hg update -C .
1433 1460 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1434 1461 $ cat > editor.sh << '__EOF__'
1435 1462 > cat "$1" | sed "s/first/very/g" > tt
1436 1463 > mv tt "$1"
1437 1464 > __EOF__
1438 1465 $ cat > newfile << '__EOF__'
1439 1466 > This is the first line
1440 1467 > This is the second line
1441 1468 > This is the third line
1442 1469 > __EOF__
1443 1470 $ hg add newfile
1444 1471 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-new <<EOF
1445 1472 > y
1446 1473 > e
1447 1474 > EOF
1448 1475 diff --git a/newfile b/newfile
1449 1476 new file mode 100644
1450 1477 examine changes to 'newfile'? [Ynesfdaq?] y
1451 1478
1452 1479 @@ -0,0 +1,3 @@
1453 1480 +This is the first line
1454 1481 +This is the second line
1455 1482 +This is the third line
1456 1483 record this change to 'newfile'? [Ynesfdaq?] e
1457 1484
1458 1485 $ hg cat -r tip newfile
1459 1486 This is the very line
1460 1487 This is the second line
1461 1488 This is the third line
1462 1489
1463 1490 $ cat newfile
1464 1491 This is the first line
1465 1492 This is the second line
1466 1493 This is the third line
1467 1494
1468 1495 Add new file from within a subdirectory
1469 1496 $ hg update -C .
1470 1497 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1471 1498 $ mkdir folder
1472 1499 $ cd folder
1473 1500 $ echo "foo" > bar
1474 1501 $ hg add bar
1475 1502 $ hg commit -i -d '23 0' -mnewfilesubdir <<EOF
1476 1503 > y
1477 1504 > y
1478 1505 > EOF
1479 1506 diff --git a/folder/bar b/folder/bar
1480 1507 new file mode 100644
1481 1508 examine changes to 'folder/bar'? [Ynesfdaq?] y
1482 1509
1483 1510 @@ -0,0 +1,1 @@
1484 1511 +foo
1485 1512 record this change to 'folder/bar'? [Ynesfdaq?] y
1486 1513
1487 1514 The #if execbit block above changes the hashes here on some systems
1488 1515 $ hg tip -p
1489 1516 changeset: 32:* (glob)
1490 1517 tag: tip
1491 1518 user: test
1492 1519 date: Thu Jan 01 00:00:23 1970 +0000
1493 1520 summary: newfilesubdir
1494 1521
1495 1522 diff -r * -r * folder/bar (glob)
1496 1523 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1497 1524 +++ b/folder/bar Thu Jan 01 00:00:23 1970 +0000
1498 1525 @@ -0,0 +1,1 @@
1499 1526 +foo
1500 1527
1501 1528 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now