##// END OF EJS Templates
Don't generate git patches that rename a file to multiple destinations...
Alexis S. L. Carvalho -
r3702:70c3ee22 default
parent child Browse files
Show More
@@ -1,667 +1,672 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from demandload import demandload
9 9 from i18n import gettext as _
10 10 from node import *
11 11 demandload(globals(), "base85 cmdutil mdiff util")
12 12 demandload(globals(), "cStringIO email.Parser errno os popen2 re shutil sha")
13 13 demandload(globals(), "sys tempfile zlib")
14 14
15 15 # helper functions
16 16
17 17 def copyfile(src, dst, basedir=None):
18 18 if not basedir:
19 19 basedir = os.getcwd()
20 20
21 21 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
22 22 if os.path.exists(absdst):
23 23 raise util.Abort(_("cannot create %s: destination already exists") %
24 24 dst)
25 25
26 26 targetdir = os.path.dirname(absdst)
27 27 if not os.path.isdir(targetdir):
28 28 os.makedirs(targetdir)
29 29
30 30 util.copyfile(abssrc, absdst)
31 31
32 32 # public functions
33 33
34 34 def extract(ui, fileobj):
35 35 '''extract patch from data read from fileobj.
36 36
37 37 patch can be normal patch or contained in email message.
38 38
39 39 return tuple (filename, message, user, date). any item in returned
40 40 tuple can be None. if filename is None, fileobj did not contain
41 41 patch. caller must unlink filename when done.'''
42 42
43 43 # attempt to detect the start of a patch
44 44 # (this heuristic is borrowed from quilt)
45 45 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
46 46 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
47 47 '(---|\*\*\*)[ \t])', re.MULTILINE)
48 48
49 49 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
50 50 tmpfp = os.fdopen(fd, 'w')
51 51 try:
52 52 hgpatch = False
53 53
54 54 msg = email.Parser.Parser().parse(fileobj)
55 55
56 56 message = msg['Subject']
57 57 user = msg['From']
58 58 # should try to parse msg['Date']
59 59 date = None
60 60
61 61 if message:
62 62 message = message.replace('\n\t', ' ')
63 63 ui.debug('Subject: %s\n' % message)
64 64 if user:
65 65 ui.debug('From: %s\n' % user)
66 66 diffs_seen = 0
67 67 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
68 68
69 69 for part in msg.walk():
70 70 content_type = part.get_content_type()
71 71 ui.debug('Content-Type: %s\n' % content_type)
72 72 if content_type not in ok_types:
73 73 continue
74 74 payload = part.get_payload(decode=True)
75 75 m = diffre.search(payload)
76 76 if m:
77 77 ui.debug(_('found patch at byte %d\n') % m.start(0))
78 78 diffs_seen += 1
79 79 cfp = cStringIO.StringIO()
80 80 if message:
81 81 cfp.write(message)
82 82 cfp.write('\n')
83 83 for line in payload[:m.start(0)].splitlines():
84 84 if line.startswith('# HG changeset patch'):
85 85 ui.debug(_('patch generated by hg export\n'))
86 86 hgpatch = True
87 87 # drop earlier commit message content
88 88 cfp.seek(0)
89 89 cfp.truncate()
90 90 elif hgpatch:
91 91 if line.startswith('# User '):
92 92 user = line[7:]
93 93 ui.debug('From: %s\n' % user)
94 94 elif line.startswith("# Date "):
95 95 date = line[7:]
96 96 if not line.startswith('# '):
97 97 cfp.write(line)
98 98 cfp.write('\n')
99 99 message = cfp.getvalue()
100 100 if tmpfp:
101 101 tmpfp.write(payload)
102 102 if not payload.endswith('\n'):
103 103 tmpfp.write('\n')
104 104 elif not diffs_seen and message and content_type == 'text/plain':
105 105 message += '\n' + payload
106 106 except:
107 107 tmpfp.close()
108 108 os.unlink(tmpname)
109 109 raise
110 110
111 111 tmpfp.close()
112 112 if not diffs_seen:
113 113 os.unlink(tmpname)
114 114 return None, message, user, date
115 115 return tmpname, message, user, date
116 116
117 117 def readgitpatch(patchname):
118 118 """extract git-style metadata about patches from <patchname>"""
119 119 class gitpatch:
120 120 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
121 121 def __init__(self, path):
122 122 self.path = path
123 123 self.oldpath = None
124 124 self.mode = None
125 125 self.op = 'MODIFY'
126 126 self.copymod = False
127 127 self.lineno = 0
128 128 self.binary = False
129 129
130 130 # Filter patch for git information
131 131 gitre = re.compile('diff --git a/(.*) b/(.*)')
132 132 pf = file(patchname)
133 133 gp = None
134 134 gitpatches = []
135 135 # Can have a git patch with only metadata, causing patch to complain
136 136 dopatch = False
137 137
138 138 lineno = 0
139 139 for line in pf:
140 140 lineno += 1
141 141 if line.startswith('diff --git'):
142 142 m = gitre.match(line)
143 143 if m:
144 144 if gp:
145 145 gitpatches.append(gp)
146 146 src, dst = m.group(1, 2)
147 147 gp = gitpatch(dst)
148 148 gp.lineno = lineno
149 149 elif gp:
150 150 if line.startswith('--- '):
151 151 if gp.op in ('COPY', 'RENAME'):
152 152 gp.copymod = True
153 153 dopatch = 'filter'
154 154 gitpatches.append(gp)
155 155 gp = None
156 156 if not dopatch:
157 157 dopatch = True
158 158 continue
159 159 if line.startswith('rename from '):
160 160 gp.op = 'RENAME'
161 161 gp.oldpath = line[12:].rstrip()
162 162 elif line.startswith('rename to '):
163 163 gp.path = line[10:].rstrip()
164 164 elif line.startswith('copy from '):
165 165 gp.op = 'COPY'
166 166 gp.oldpath = line[10:].rstrip()
167 167 elif line.startswith('copy to '):
168 168 gp.path = line[8:].rstrip()
169 169 elif line.startswith('deleted file'):
170 170 gp.op = 'DELETE'
171 171 elif line.startswith('new file mode '):
172 172 gp.op = 'ADD'
173 173 gp.mode = int(line.rstrip()[-3:], 8)
174 174 elif line.startswith('new mode '):
175 175 gp.mode = int(line.rstrip()[-3:], 8)
176 176 elif line.startswith('GIT binary patch'):
177 177 if not dopatch:
178 178 dopatch = 'binary'
179 179 gp.binary = True
180 180 if gp:
181 181 gitpatches.append(gp)
182 182
183 183 if not gitpatches:
184 184 dopatch = True
185 185
186 186 return (dopatch, gitpatches)
187 187
188 188 def dogitpatch(patchname, gitpatches, cwd=None):
189 189 """Preprocess git patch so that vanilla patch can handle it"""
190 190 def extractbin(fp):
191 191 line = fp.readline().rstrip()
192 192 while line and not line.startswith('literal '):
193 193 line = fp.readline().rstrip()
194 194 if not line:
195 195 return
196 196 size = int(line[8:])
197 197 dec = []
198 198 line = fp.readline().rstrip()
199 199 while line:
200 200 l = line[0]
201 201 if l <= 'Z' and l >= 'A':
202 202 l = ord(l) - ord('A') + 1
203 203 else:
204 204 l = ord(l) - ord('a') + 27
205 205 dec.append(base85.b85decode(line[1:])[:l])
206 206 line = fp.readline().rstrip()
207 207 text = zlib.decompress(''.join(dec))
208 208 if len(text) != size:
209 209 raise util.Abort(_('binary patch is %d bytes, not %d') %
210 210 (len(text), size))
211 211 return text
212 212
213 213 pf = file(patchname)
214 214 pfline = 1
215 215
216 216 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
217 217 tmpfp = os.fdopen(fd, 'w')
218 218
219 219 try:
220 220 for i in xrange(len(gitpatches)):
221 221 p = gitpatches[i]
222 222 if not p.copymod and not p.binary:
223 223 continue
224 224
225 225 # rewrite patch hunk
226 226 while pfline < p.lineno:
227 227 tmpfp.write(pf.readline())
228 228 pfline += 1
229 229
230 230 if p.binary:
231 231 text = extractbin(pf)
232 232 if not text:
233 233 raise util.Abort(_('binary patch extraction failed'))
234 234 if not cwd:
235 235 cwd = os.getcwd()
236 236 absdst = os.path.join(cwd, p.path)
237 237 basedir = os.path.dirname(absdst)
238 238 if not os.path.isdir(basedir):
239 239 os.makedirs(basedir)
240 240 out = file(absdst, 'wb')
241 241 out.write(text)
242 242 out.close()
243 243 elif p.copymod:
244 244 copyfile(p.oldpath, p.path, basedir=cwd)
245 245 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
246 246 line = pf.readline()
247 247 pfline += 1
248 248 while not line.startswith('--- a/'):
249 249 tmpfp.write(line)
250 250 line = pf.readline()
251 251 pfline += 1
252 252 tmpfp.write('--- a/%s\n' % p.path)
253 253
254 254 line = pf.readline()
255 255 while line:
256 256 tmpfp.write(line)
257 257 line = pf.readline()
258 258 except:
259 259 tmpfp.close()
260 260 os.unlink(patchname)
261 261 raise
262 262
263 263 tmpfp.close()
264 264 return patchname
265 265
266 266 def patch(patchname, ui, strip=1, cwd=None, files={}):
267 267 """apply the patch <patchname> to the working directory.
268 268 a list of patched files is returned"""
269 269
270 270 # helper function
271 271 def __patch(patchname):
272 272 """patch and updates the files and fuzz variables"""
273 273 fuzz = False
274 274
275 275 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
276 276 'patch')
277 277 args = []
278 278 if cwd:
279 279 args.append('-d %s' % util.shellquote(cwd))
280 280 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
281 281 util.shellquote(patchname)))
282 282
283 283 for line in fp:
284 284 line = line.rstrip()
285 285 ui.note(line + '\n')
286 286 if line.startswith('patching file '):
287 287 pf = util.parse_patch_output(line)
288 288 printed_file = False
289 289 files.setdefault(pf, (None, None))
290 290 elif line.find('with fuzz') >= 0:
291 291 fuzz = True
292 292 if not printed_file:
293 293 ui.warn(pf + '\n')
294 294 printed_file = True
295 295 ui.warn(line + '\n')
296 296 elif line.find('saving rejects to file') >= 0:
297 297 ui.warn(line + '\n')
298 298 elif line.find('FAILED') >= 0:
299 299 if not printed_file:
300 300 ui.warn(pf + '\n')
301 301 printed_file = True
302 302 ui.warn(line + '\n')
303 303 code = fp.close()
304 304 if code:
305 305 raise util.Abort(_("patch command failed: %s") %
306 306 util.explain_exit(code)[0])
307 307 return fuzz
308 308
309 309 (dopatch, gitpatches) = readgitpatch(patchname)
310 310 for gp in gitpatches:
311 311 files[gp.path] = (gp.op, gp)
312 312
313 313 fuzz = False
314 314 if dopatch:
315 315 if dopatch in ('filter', 'binary'):
316 316 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
317 317 try:
318 318 if dopatch != 'binary':
319 319 fuzz = __patch(patchname)
320 320 finally:
321 321 if dopatch == 'filter':
322 322 os.unlink(patchname)
323 323
324 324 return fuzz
325 325
326 326 def diffopts(ui, opts={}, untrusted=False):
327 327 def get(key, name=None):
328 328 return (opts.get(key) or
329 329 ui.configbool('diff', name or key, None, untrusted=untrusted))
330 330 return mdiff.diffopts(
331 331 text=opts.get('text'),
332 332 git=get('git'),
333 333 nodates=get('nodates'),
334 334 showfunc=get('show_function', 'showfunc'),
335 335 ignorews=get('ignore_all_space', 'ignorews'),
336 336 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
337 337 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
338 338
339 339 def updatedir(ui, repo, patches, wlock=None):
340 340 '''Update dirstate after patch application according to metadata'''
341 341 if not patches:
342 342 return
343 343 copies = []
344 344 removes = {}
345 345 cfiles = patches.keys()
346 346 cwd = repo.getcwd()
347 347 if cwd:
348 348 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
349 349 for f in patches:
350 350 ctype, gp = patches[f]
351 351 if ctype == 'RENAME':
352 352 copies.append((gp.oldpath, gp.path, gp.copymod))
353 353 removes[gp.oldpath] = 1
354 354 elif ctype == 'COPY':
355 355 copies.append((gp.oldpath, gp.path, gp.copymod))
356 356 elif ctype == 'DELETE':
357 357 removes[gp.path] = 1
358 358 for src, dst, after in copies:
359 359 if not after:
360 360 copyfile(src, dst, repo.root)
361 361 repo.copy(src, dst, wlock=wlock)
362 362 removes = removes.keys()
363 363 if removes:
364 364 removes.sort()
365 365 repo.remove(removes, True, wlock=wlock)
366 366 for f in patches:
367 367 ctype, gp = patches[f]
368 368 if gp and gp.mode:
369 369 x = gp.mode & 0100 != 0
370 370 dst = os.path.join(repo.root, gp.path)
371 371 # patch won't create empty files
372 372 if ctype == 'ADD' and not os.path.exists(dst):
373 373 repo.wwrite(gp.path, '')
374 374 util.set_exec(dst, x)
375 375 cmdutil.addremove(repo, cfiles, wlock=wlock)
376 376 files = patches.keys()
377 377 files.extend([r for r in removes if r not in files])
378 378 files.sort()
379 379
380 380 return files
381 381
382 382 def b85diff(fp, to, tn):
383 383 '''print base85-encoded binary diff'''
384 384 def gitindex(text):
385 385 if not text:
386 386 return '0' * 40
387 387 l = len(text)
388 388 s = sha.new('blob %d\0' % l)
389 389 s.update(text)
390 390 return s.hexdigest()
391 391
392 392 def fmtline(line):
393 393 l = len(line)
394 394 if l <= 26:
395 395 l = chr(ord('A') + l - 1)
396 396 else:
397 397 l = chr(l - 26 + ord('a') - 1)
398 398 return '%c%s\n' % (l, base85.b85encode(line, True))
399 399
400 400 def chunk(text, csize=52):
401 401 l = len(text)
402 402 i = 0
403 403 while i < l:
404 404 yield text[i:i+csize]
405 405 i += csize
406 406
407 407 # TODO: deltas
408 408 l = len(tn)
409 409 fp.write('index %s..%s\nGIT binary patch\nliteral %s\n' %
410 410 (gitindex(to), gitindex(tn), len(tn)))
411 411
412 412 tn = ''.join([fmtline(l) for l in chunk(zlib.compress(tn))])
413 413 fp.write(tn)
414 414 fp.write('\n')
415 415
416 416 def diff(repo, node1=None, node2=None, files=None, match=util.always,
417 417 fp=None, changes=None, opts=None):
418 418 '''print diff of changes to files between two nodes, or node and
419 419 working directory.
420 420
421 421 if node1 is None, use first dirstate parent instead.
422 422 if node2 is None, compare node1 with working directory.'''
423 423
424 424 if opts is None:
425 425 opts = mdiff.defaultopts
426 426 if fp is None:
427 427 fp = repo.ui
428 428
429 429 if not node1:
430 430 node1 = repo.dirstate.parents()[0]
431 431
432 432 clcache = {}
433 433 def getchangelog(n):
434 434 if n not in clcache:
435 435 clcache[n] = repo.changelog.read(n)
436 436 return clcache[n]
437 437 mcache = {}
438 438 def getmanifest(n):
439 439 if n not in mcache:
440 440 mcache[n] = repo.manifest.read(n)
441 441 return mcache[n]
442 442 fcache = {}
443 443 def getfile(f):
444 444 if f not in fcache:
445 445 fcache[f] = repo.file(f)
446 446 return fcache[f]
447 447
448 448 # reading the data for node1 early allows it to play nicely
449 449 # with repo.status and the revlog cache.
450 450 change = getchangelog(node1)
451 451 mmap = getmanifest(change[0])
452 452 date1 = util.datestr(change[2])
453 453
454 454 if not changes:
455 455 changes = repo.status(node1, node2, files, match=match)[:5]
456 456 modified, added, removed, deleted, unknown = changes
457 457 if files:
458 458 def filterfiles(filters):
459 459 l = [x for x in filters if x in files]
460 460
461 461 for t in files:
462 462 if not t.endswith("/"):
463 463 t += "/"
464 464 l += [x for x in filters if x.startswith(t)]
465 465 return l
466 466
467 467 modified, added, removed = map(filterfiles, (modified, added, removed))
468 468
469 469 if not modified and not added and not removed:
470 470 return
471 471
472 472 # returns False if there was no rename between n1 and n2
473 473 # returns None if the file was created between n1 and n2
474 474 # returns the (file, node) present in n1 that was renamed to f in n2
475 475 def renamedbetween(f, n1, n2):
476 476 r1, r2 = map(repo.changelog.rev, (n1, n2))
477 477 orig = f
478 478 src = None
479 479 while r2 > r1:
480 480 cl = getchangelog(n2)
481 481 if f in cl[3]:
482 482 m = getmanifest(cl[0])
483 483 try:
484 484 src = getfile(f).renamed(m[f])
485 485 except KeyError:
486 486 return None
487 487 if src:
488 488 f = src[0]
489 489 n2 = repo.changelog.parents(n2)[0]
490 490 r2 = repo.changelog.rev(n2)
491 491 cl = getchangelog(n1)
492 492 m = getmanifest(cl[0])
493 493 if f not in m:
494 494 return None
495 495 if f == orig:
496 496 return False
497 497 return f, m[f]
498 498
499 499 if node2:
500 500 change = getchangelog(node2)
501 501 mmap2 = getmanifest(change[0])
502 502 _date2 = util.datestr(change[2])
503 503 def date2(f):
504 504 return _date2
505 505 def read(f):
506 506 return getfile(f).read(mmap2[f])
507 507 def renamed(f):
508 508 return renamedbetween(f, node1, node2)
509 509 else:
510 510 tz = util.makedate()[1]
511 511 _date2 = util.datestr()
512 512 def date2(f):
513 513 try:
514 514 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
515 515 except OSError, err:
516 516 if err.errno != errno.ENOENT: raise
517 517 return _date2
518 518 def read(f):
519 519 return repo.wread(f)
520 520 def renamed(f):
521 521 src = repo.dirstate.copied(f)
522 522 parent = repo.dirstate.parents()[0]
523 523 if src:
524 524 f = src
525 525 of = renamedbetween(f, node1, parent)
526 526 if of or of is None:
527 527 return of
528 528 elif src:
529 529 cl = getchangelog(parent)[0]
530 530 return (src, getmanifest(cl)[src])
531 531 else:
532 532 return None
533 533
534 534 if repo.ui.quiet:
535 535 r = None
536 536 else:
537 537 hexfunc = repo.ui.debugflag and hex or short
538 538 r = [hexfunc(node) for node in [node1, node2] if node]
539 539
540 540 if opts.git:
541 541 copied = {}
542 542 for f in added:
543 543 src = renamed(f)
544 544 if src:
545 545 copied[f] = src
546 546 srcs = [x[1][0] for x in copied.items()]
547 547
548 548 all = modified + added + removed
549 549 all.sort()
550 gone = {}
550 551 for f in all:
551 552 to = None
552 553 tn = None
553 554 dodiff = True
554 555 header = []
555 556 if f in mmap:
556 557 to = getfile(f).read(mmap[f])
557 558 if f not in removed:
558 559 tn = read(f)
559 560 if opts.git:
560 561 def gitmode(x):
561 562 return x and '100755' or '100644'
562 563 def addmodehdr(header, omode, nmode):
563 564 if omode != nmode:
564 565 header.append('old mode %s\n' % omode)
565 566 header.append('new mode %s\n' % nmode)
566 567
567 568 a, b = f, f
568 569 if f in added:
569 570 if node2:
570 571 mode = gitmode(mmap2.execf(f))
571 572 else:
572 573 mode = gitmode(util.is_exec(repo.wjoin(f), None))
573 574 if f in copied:
574 575 a, arev = copied[f]
575 576 omode = gitmode(mmap.execf(a))
576 577 addmodehdr(header, omode, mode)
577 op = a in removed and 'rename' or 'copy'
578 if a in removed and a not in gone:
579 op = 'rename'
580 gone[a] = 1
581 else:
582 op = 'copy'
578 583 header.append('%s from %s\n' % (op, a))
579 584 header.append('%s to %s\n' % (op, f))
580 585 to = getfile(a).read(arev)
581 586 else:
582 587 header.append('new file mode %s\n' % mode)
583 588 if util.binary(tn):
584 589 dodiff = 'binary'
585 590 elif f in removed:
586 591 if f in srcs:
587 592 dodiff = False
588 593 else:
589 594 mode = gitmode(mmap.execf(f))
590 595 header.append('deleted file mode %s\n' % mode)
591 596 else:
592 597 omode = gitmode(mmap.execf(f))
593 598 if node2:
594 599 nmode = gitmode(mmap2.execf(f))
595 600 else:
596 601 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
597 602 addmodehdr(header, omode, nmode)
598 603 if util.binary(to) or util.binary(tn):
599 604 dodiff = 'binary'
600 605 r = None
601 606 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
602 607 if dodiff == 'binary':
603 608 fp.write(''.join(header))
604 609 b85diff(fp, to, tn)
605 610 elif dodiff:
606 611 text = mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts)
607 612 if text or len(header) > 1:
608 613 fp.write(''.join(header))
609 614 fp.write(text)
610 615
611 616 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
612 617 opts=None):
613 618 '''export changesets as hg patches.'''
614 619
615 620 total = len(revs)
616 621 revwidth = max(map(len, revs))
617 622
618 623 def single(node, seqno, fp):
619 624 parents = [p for p in repo.changelog.parents(node) if p != nullid]
620 625 if switch_parent:
621 626 parents.reverse()
622 627 prev = (parents and parents[0]) or nullid
623 628 change = repo.changelog.read(node)
624 629
625 630 if not fp:
626 631 fp = cmdutil.make_file(repo, template, node, total=total,
627 632 seqno=seqno, revwidth=revwidth)
628 633 if fp not in (sys.stdout, repo.ui):
629 634 repo.ui.note("%s\n" % fp.name)
630 635
631 636 fp.write("# HG changeset patch\n")
632 637 fp.write("# User %s\n" % change[1])
633 638 fp.write("# Date %d %d\n" % change[2])
634 639 fp.write("# Node ID %s\n" % hex(node))
635 640 fp.write("# Parent %s\n" % hex(prev))
636 641 if len(parents) > 1:
637 642 fp.write("# Parent %s\n" % hex(parents[1]))
638 643 fp.write(change[4].rstrip())
639 644 fp.write("\n\n")
640 645
641 646 diff(repo, prev, node, fp=fp, opts=opts)
642 647 if fp not in (sys.stdout, repo.ui):
643 648 fp.close()
644 649
645 650 for seqno, cset in enumerate(revs):
646 651 single(cset, seqno, fp)
647 652
648 653 def diffstat(patchlines):
649 654 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
650 655 try:
651 656 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
652 657 try:
653 658 for line in patchlines: print >> p.tochild, line
654 659 p.tochild.close()
655 660 if p.wait(): return
656 661 fp = os.fdopen(fd, 'r')
657 662 stat = []
658 663 for line in fp: stat.append(line.lstrip())
659 664 last = stat.pop()
660 665 stat.insert(0, last)
661 666 stat = ''.join(stat)
662 667 if stat.startswith('0 files'): raise ValueError
663 668 return stat
664 669 except: raise
665 670 finally:
666 671 try: os.unlink(name)
667 672 except: pass
@@ -1,129 +1,137 b''
1 1 #!/bin/sh
2 2
3 3 hg init a
4 4 cd a
5 5
6 6 echo start > start
7 7 hg ci -Amstart -d '0 0'
8 8 echo new > new
9 9 hg ci -Amnew -d '0 0'
10 10 echo '% new file'
11 11 hg diff --git -r 0
12 12
13 13 hg cp new copy
14 14 hg ci -mcopy -d '0 0'
15 15 echo '% copy'
16 16 hg diff --git -r 1:tip
17 17
18 18 hg mv copy rename
19 19 hg ci -mrename -d '0 0'
20 20 echo '% rename'
21 21 hg diff --git -r 2:tip
22 22
23 23 hg rm rename
24 24 hg ci -mdelete -d '0 0'
25 25 echo '% delete'
26 26 hg diff --git -r 3:tip
27 27
28 28 cat > src <<EOF
29 29 1
30 30 2
31 31 3
32 32 4
33 33 5
34 34 EOF
35 35 hg ci -Amsrc -d '0 0'
36 36 chmod +x src
37 37 hg ci -munexec -d '0 0'
38 38 echo '% chmod 644'
39 39 hg diff --git -r 5:tip
40 40
41 41 hg mv src dst
42 42 chmod -x dst
43 43 echo a >> dst
44 44 hg ci -mrenamemod -d '0 0'
45 45 echo '% rename+mod+chmod'
46 46 hg diff --git -r 6:tip
47 47
48 48 echo '% nonexistent in tip+chmod'
49 49 hg diff --git -r 5:6
50 50
51 51 echo '% binary diff'
52 52 cp $TESTDIR/binfile.bin .
53 53 hg add binfile.bin
54 54 hg diff --git > b.diff
55 55 cat b.diff
56 56
57 57 echo '% import binary diff'
58 58 hg revert binfile.bin
59 59 rm binfile.bin
60 60 hg import -mfoo b.diff
61 61 cmp binfile.bin $TESTDIR/binfile.bin
62 62
63 63 echo
64 64 echo '% diff across many revisions'
65 65 hg mv dst dst2
66 66 hg ci -m 'mv dst dst2' -d '0 0'
67 67
68 68 echo >> start
69 69 hg ci -m 'change start' -d '0 0'
70 70
71 71 hg revert -r -2 start
72 72 hg mv dst2 dst3
73 73 hg ci -m 'mv dst2 dst3; revert start' -d '0 0'
74 74
75 75 hg diff --git -r 9:11
76 76
77 77 echo a >> foo
78 78 hg add foo
79 79 hg ci -m 'add foo'
80 80 echo b >> foo
81 81 hg ci -m 'change foo'
82 82 hg mv foo bar
83 83 hg ci -m 'mv foo bar'
84 84 echo c >> bar
85 85 hg ci -m 'change bar'
86 86
87 87 echo
88 88 echo '% file created before r1 and renamed before r2'
89 89 hg diff --git -r -3:-1
90 90 echo
91 91 echo '% file created in r1 and renamed before r2'
92 92 hg diff --git -r -4:-1
93 93 echo
94 94 echo '% file created after r1 and renamed before r2'
95 95 hg diff --git -r -5:-1
96 96
97 97 echo
98 98 echo '% comparing with the working dir'
99 99 echo >> start
100 100 hg ci -m 'change start again' -d '0 0'
101 101
102 102 echo > created
103 103 hg add created
104 104 hg ci -m 'add created'
105 105
106 106 hg mv created created2
107 107 hg ci -m 'mv created created2'
108 108
109 109 hg mv created2 created3
110 110 echo "% there's a copy in the working dir..."
111 111 hg diff --git
112 112 echo
113 113 echo "% ...but there's another copy between the original rev and the wd"
114 114 hg diff --git -r -2
115 115 echo
116 116 echo "% ...but the source of the copy was created after the original rev"
117 117 hg diff --git -r -3
118 118 hg ci -m 'mv created2 created3'
119 119
120 120 echo > brand-new
121 121 hg add brand-new
122 122 hg ci -m 'add brand-new'
123 123 hg mv brand-new brand-new2
124 124 echo '% created in parent of wd; renamed in the wd'
125 125 hg diff --git
126 126
127 127 echo
128 128 echo '% created between r1 and parent of wd; renamed in the wd'
129 129 hg diff --git -r -2
130 hg ci -m 'mv brand-new brand-new2'
131
132 echo '% one file is copied to many destinations and removed'
133 hg cp brand-new2 brand-new3
134 hg mv brand-new2 brand-new3-2
135 hg ci -m 'multiple renames/copies'
136 hg diff --git -r -2 -r -1
137
@@ -1,135 +1,142 b''
1 1 adding start
2 2 adding new
3 3 % new file
4 4 diff --git a/new b/new
5 5 new file mode 100644
6 6 --- /dev/null
7 7 +++ b/new
8 8 @@ -0,0 +1,1 @@
9 9 +new
10 10 % copy
11 11 diff --git a/new b/copy
12 12 copy from new
13 13 copy to copy
14 14 % rename
15 15 diff --git a/copy b/rename
16 16 rename from copy
17 17 rename to rename
18 18 % delete
19 19 diff --git a/rename b/rename
20 20 deleted file mode 100644
21 21 --- a/rename
22 22 +++ /dev/null
23 23 @@ -1,1 +0,0 @@
24 24 -new
25 25 adding src
26 26 % chmod 644
27 27 diff --git a/src b/src
28 28 old mode 100644
29 29 new mode 100755
30 30 % rename+mod+chmod
31 31 diff --git a/src b/dst
32 32 old mode 100755
33 33 new mode 100644
34 34 rename from src
35 35 rename to dst
36 36 --- a/dst
37 37 +++ b/dst
38 38 @@ -3,3 +3,4 @@ 3
39 39 3
40 40 4
41 41 5
42 42 +a
43 43 % nonexistent in tip+chmod
44 44 diff --git a/src b/src
45 45 old mode 100644
46 46 new mode 100755
47 47 % binary diff
48 48 diff --git a/binfile.bin b/binfile.bin
49 49 new file mode 100644
50 50 index 0000000000000000000000000000000000000000..37ba3d1c6f17137d9c5f5776fa040caf5fe73ff9
51 51 GIT binary patch
52 52 literal 593
53 53 zc$@)I0<QguP)<h;3K|Lk000e1NJLTq000mG000mO0ssI2kdbIM00009a7bBm000XU
54 54 z000XU0RWnu7ytkO2XskIMF-Uh9TW;VpMjwv0005-Nkl<ZD9@FWPs=e;7{<>W$NUkd
55 55 zX$nnYLt$-$V!?uy+1V%`z&Eh=ah|duER<4|QWhju3gb^nF*8iYobxWG-qqXl=2~5M
56 56 z*IoDB)sG^CfNuoBmqLTVU^<;@nwHP!1wrWd`{(mHo6VNXWtyh{alzqmsH*yYzpvLT
57 57 zLdY<T=ks|woh-`&01!ej#(xbV1f|pI*=%;d-%F*E*X#ZH`4I%6SS+$EJDE&ct=8po
58 58 ziN#{?_j|kD%Cd|oiqds`xm@;oJ-^?NG3Gdqrs?5u*zI;{nogxsx~^|Fn^Y?Gdc6<;
59 59 zfMJ+iF1J`LMx&A2?dEwNW8ClebzPTbIh{@$hS6*`kH@1d%Lo7fA#}N1)oN7`gm$~V
60 60 z+wDx#)OFqMcE{s!JN0-xhG8ItAjVkJwEcb`3WWlJfU2r?;Pd%dmR+q@mSri5q9_W-
61 61 zaR2~ECX?B2w+zELozC0s*6Z~|QG^f{3I#<`?)Q7U-JZ|q5W;9Q8i_=pBuSzunx=U;
62 62 z9C)5jBoYw9^?EHyQl(M}1OlQcCX>lXB*ODN003Z&P17_@)3Pi=i0wb04<W?v-u}7K
63 63 zXmmQA+wDgE!qR9o8jr`%=ab_&uh(l?R=r;Tjiqon91I2-hIu?57~@*4h7h9uORK#=
64 64 fQItJW-{SoTm)8|5##k|m00000NkvXXu0mjf{mKw4
65 65
66 66 % import binary diff
67 67 applying b.diff
68 68
69 69 % diff across many revisions
70 70 diff --git a/dst2 b/dst3
71 71 rename from dst2
72 72 rename to dst3
73 73
74 74 % file created before r1 and renamed before r2
75 75 diff --git a/foo b/bar
76 76 rename from foo
77 77 rename to bar
78 78 --- a/bar
79 79 +++ b/bar
80 80 @@ -1,2 +1,3 @@ a
81 81 a
82 82 b
83 83 +c
84 84
85 85 % file created in r1 and renamed before r2
86 86 diff --git a/foo b/bar
87 87 rename from foo
88 88 rename to bar
89 89 --- a/bar
90 90 +++ b/bar
91 91 @@ -1,1 +1,3 @@ a
92 92 a
93 93 +b
94 94 +c
95 95
96 96 % file created after r1 and renamed before r2
97 97 diff --git a/bar b/bar
98 98 new file mode 100644
99 99 --- /dev/null
100 100 +++ b/bar
101 101 @@ -0,0 +1,3 @@
102 102 +a
103 103 +b
104 104 +c
105 105
106 106 % comparing with the working dir
107 107 % there's a copy in the working dir...
108 108 diff --git a/created2 b/created3
109 109 rename from created2
110 110 rename to created3
111 111
112 112 % ...but there's another copy between the original rev and the wd
113 113 diff --git a/created b/created3
114 114 rename from created
115 115 rename to created3
116 116
117 117 % ...but the source of the copy was created after the original rev
118 118 diff --git a/created3 b/created3
119 119 new file mode 100644
120 120 --- /dev/null
121 121 +++ b/created3
122 122 @@ -0,0 +1,1 @@
123 123 +
124 124 % created in parent of wd; renamed in the wd
125 125 diff --git a/brand-new b/brand-new2
126 126 rename from brand-new
127 127 rename to brand-new2
128 128
129 129 % created between r1 and parent of wd; renamed in the wd
130 130 diff --git a/brand-new2 b/brand-new2
131 131 new file mode 100644
132 132 --- /dev/null
133 133 +++ b/brand-new2
134 134 @@ -0,0 +1,1 @@
135 135 +
136 % one file is copied to many destinations and removed
137 diff --git a/brand-new2 b/brand-new3
138 rename from brand-new2
139 rename to brand-new3
140 diff --git a/brand-new2 b/brand-new3-2
141 copy from brand-new2
142 copy to brand-new3-2
General Comments 0
You need to be logged in to leave comments. Login now