##// END OF EJS Templates
handle git patches that rename a file to more than one destination
Alexis S. L. Carvalho -
r3701:05c8704a default
parent child Browse files
Show More
@@ -1,665 +1,667
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 removes = []
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 removes.append(gp.oldpath)
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 removes.append(gp.path)
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 removes = removes.keys()
362 363 if removes:
364 removes.sort()
363 365 repo.remove(removes, True, wlock=wlock)
364 366 for f in patches:
365 367 ctype, gp = patches[f]
366 368 if gp and gp.mode:
367 369 x = gp.mode & 0100 != 0
368 370 dst = os.path.join(repo.root, gp.path)
369 371 # patch won't create empty files
370 372 if ctype == 'ADD' and not os.path.exists(dst):
371 373 repo.wwrite(gp.path, '')
372 374 util.set_exec(dst, x)
373 375 cmdutil.addremove(repo, cfiles, wlock=wlock)
374 376 files = patches.keys()
375 377 files.extend([r for r in removes if r not in files])
376 378 files.sort()
377 379
378 380 return files
379 381
380 382 def b85diff(fp, to, tn):
381 383 '''print base85-encoded binary diff'''
382 384 def gitindex(text):
383 385 if not text:
384 386 return '0' * 40
385 387 l = len(text)
386 388 s = sha.new('blob %d\0' % l)
387 389 s.update(text)
388 390 return s.hexdigest()
389 391
390 392 def fmtline(line):
391 393 l = len(line)
392 394 if l <= 26:
393 395 l = chr(ord('A') + l - 1)
394 396 else:
395 397 l = chr(l - 26 + ord('a') - 1)
396 398 return '%c%s\n' % (l, base85.b85encode(line, True))
397 399
398 400 def chunk(text, csize=52):
399 401 l = len(text)
400 402 i = 0
401 403 while i < l:
402 404 yield text[i:i+csize]
403 405 i += csize
404 406
405 407 # TODO: deltas
406 408 l = len(tn)
407 409 fp.write('index %s..%s\nGIT binary patch\nliteral %s\n' %
408 410 (gitindex(to), gitindex(tn), len(tn)))
409 411
410 412 tn = ''.join([fmtline(l) for l in chunk(zlib.compress(tn))])
411 413 fp.write(tn)
412 414 fp.write('\n')
413 415
414 416 def diff(repo, node1=None, node2=None, files=None, match=util.always,
415 417 fp=None, changes=None, opts=None):
416 418 '''print diff of changes to files between two nodes, or node and
417 419 working directory.
418 420
419 421 if node1 is None, use first dirstate parent instead.
420 422 if node2 is None, compare node1 with working directory.'''
421 423
422 424 if opts is None:
423 425 opts = mdiff.defaultopts
424 426 if fp is None:
425 427 fp = repo.ui
426 428
427 429 if not node1:
428 430 node1 = repo.dirstate.parents()[0]
429 431
430 432 clcache = {}
431 433 def getchangelog(n):
432 434 if n not in clcache:
433 435 clcache[n] = repo.changelog.read(n)
434 436 return clcache[n]
435 437 mcache = {}
436 438 def getmanifest(n):
437 439 if n not in mcache:
438 440 mcache[n] = repo.manifest.read(n)
439 441 return mcache[n]
440 442 fcache = {}
441 443 def getfile(f):
442 444 if f not in fcache:
443 445 fcache[f] = repo.file(f)
444 446 return fcache[f]
445 447
446 448 # reading the data for node1 early allows it to play nicely
447 449 # with repo.status and the revlog cache.
448 450 change = getchangelog(node1)
449 451 mmap = getmanifest(change[0])
450 452 date1 = util.datestr(change[2])
451 453
452 454 if not changes:
453 455 changes = repo.status(node1, node2, files, match=match)[:5]
454 456 modified, added, removed, deleted, unknown = changes
455 457 if files:
456 458 def filterfiles(filters):
457 459 l = [x for x in filters if x in files]
458 460
459 461 for t in files:
460 462 if not t.endswith("/"):
461 463 t += "/"
462 464 l += [x for x in filters if x.startswith(t)]
463 465 return l
464 466
465 467 modified, added, removed = map(filterfiles, (modified, added, removed))
466 468
467 469 if not modified and not added and not removed:
468 470 return
469 471
470 472 # returns False if there was no rename between n1 and n2
471 473 # returns None if the file was created between n1 and n2
472 474 # returns the (file, node) present in n1 that was renamed to f in n2
473 475 def renamedbetween(f, n1, n2):
474 476 r1, r2 = map(repo.changelog.rev, (n1, n2))
475 477 orig = f
476 478 src = None
477 479 while r2 > r1:
478 480 cl = getchangelog(n2)
479 481 if f in cl[3]:
480 482 m = getmanifest(cl[0])
481 483 try:
482 484 src = getfile(f).renamed(m[f])
483 485 except KeyError:
484 486 return None
485 487 if src:
486 488 f = src[0]
487 489 n2 = repo.changelog.parents(n2)[0]
488 490 r2 = repo.changelog.rev(n2)
489 491 cl = getchangelog(n1)
490 492 m = getmanifest(cl[0])
491 493 if f not in m:
492 494 return None
493 495 if f == orig:
494 496 return False
495 497 return f, m[f]
496 498
497 499 if node2:
498 500 change = getchangelog(node2)
499 501 mmap2 = getmanifest(change[0])
500 502 _date2 = util.datestr(change[2])
501 503 def date2(f):
502 504 return _date2
503 505 def read(f):
504 506 return getfile(f).read(mmap2[f])
505 507 def renamed(f):
506 508 return renamedbetween(f, node1, node2)
507 509 else:
508 510 tz = util.makedate()[1]
509 511 _date2 = util.datestr()
510 512 def date2(f):
511 513 try:
512 514 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
513 515 except OSError, err:
514 516 if err.errno != errno.ENOENT: raise
515 517 return _date2
516 518 def read(f):
517 519 return repo.wread(f)
518 520 def renamed(f):
519 521 src = repo.dirstate.copied(f)
520 522 parent = repo.dirstate.parents()[0]
521 523 if src:
522 524 f = src
523 525 of = renamedbetween(f, node1, parent)
524 526 if of or of is None:
525 527 return of
526 528 elif src:
527 529 cl = getchangelog(parent)[0]
528 530 return (src, getmanifest(cl)[src])
529 531 else:
530 532 return None
531 533
532 534 if repo.ui.quiet:
533 535 r = None
534 536 else:
535 537 hexfunc = repo.ui.debugflag and hex or short
536 538 r = [hexfunc(node) for node in [node1, node2] if node]
537 539
538 540 if opts.git:
539 541 copied = {}
540 542 for f in added:
541 543 src = renamed(f)
542 544 if src:
543 545 copied[f] = src
544 546 srcs = [x[1][0] for x in copied.items()]
545 547
546 548 all = modified + added + removed
547 549 all.sort()
548 550 for f in all:
549 551 to = None
550 552 tn = None
551 553 dodiff = True
552 554 header = []
553 555 if f in mmap:
554 556 to = getfile(f).read(mmap[f])
555 557 if f not in removed:
556 558 tn = read(f)
557 559 if opts.git:
558 560 def gitmode(x):
559 561 return x and '100755' or '100644'
560 562 def addmodehdr(header, omode, nmode):
561 563 if omode != nmode:
562 564 header.append('old mode %s\n' % omode)
563 565 header.append('new mode %s\n' % nmode)
564 566
565 567 a, b = f, f
566 568 if f in added:
567 569 if node2:
568 570 mode = gitmode(mmap2.execf(f))
569 571 else:
570 572 mode = gitmode(util.is_exec(repo.wjoin(f), None))
571 573 if f in copied:
572 574 a, arev = copied[f]
573 575 omode = gitmode(mmap.execf(a))
574 576 addmodehdr(header, omode, mode)
575 577 op = a in removed and 'rename' or 'copy'
576 578 header.append('%s from %s\n' % (op, a))
577 579 header.append('%s to %s\n' % (op, f))
578 580 to = getfile(a).read(arev)
579 581 else:
580 582 header.append('new file mode %s\n' % mode)
581 583 if util.binary(tn):
582 584 dodiff = 'binary'
583 585 elif f in removed:
584 586 if f in srcs:
585 587 dodiff = False
586 588 else:
587 589 mode = gitmode(mmap.execf(f))
588 590 header.append('deleted file mode %s\n' % mode)
589 591 else:
590 592 omode = gitmode(mmap.execf(f))
591 593 if node2:
592 594 nmode = gitmode(mmap2.execf(f))
593 595 else:
594 596 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
595 597 addmodehdr(header, omode, nmode)
596 598 if util.binary(to) or util.binary(tn):
597 599 dodiff = 'binary'
598 600 r = None
599 601 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
600 602 if dodiff == 'binary':
601 603 fp.write(''.join(header))
602 604 b85diff(fp, to, tn)
603 605 elif dodiff:
604 606 text = mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts)
605 607 if text or len(header) > 1:
606 608 fp.write(''.join(header))
607 609 fp.write(text)
608 610
609 611 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
610 612 opts=None):
611 613 '''export changesets as hg patches.'''
612 614
613 615 total = len(revs)
614 616 revwidth = max(map(len, revs))
615 617
616 618 def single(node, seqno, fp):
617 619 parents = [p for p in repo.changelog.parents(node) if p != nullid]
618 620 if switch_parent:
619 621 parents.reverse()
620 622 prev = (parents and parents[0]) or nullid
621 623 change = repo.changelog.read(node)
622 624
623 625 if not fp:
624 626 fp = cmdutil.make_file(repo, template, node, total=total,
625 627 seqno=seqno, revwidth=revwidth)
626 628 if fp not in (sys.stdout, repo.ui):
627 629 repo.ui.note("%s\n" % fp.name)
628 630
629 631 fp.write("# HG changeset patch\n")
630 632 fp.write("# User %s\n" % change[1])
631 633 fp.write("# Date %d %d\n" % change[2])
632 634 fp.write("# Node ID %s\n" % hex(node))
633 635 fp.write("# Parent %s\n" % hex(prev))
634 636 if len(parents) > 1:
635 637 fp.write("# Parent %s\n" % hex(parents[1]))
636 638 fp.write(change[4].rstrip())
637 639 fp.write("\n\n")
638 640
639 641 diff(repo, prev, node, fp=fp, opts=opts)
640 642 if fp not in (sys.stdout, repo.ui):
641 643 fp.close()
642 644
643 645 for seqno, cset in enumerate(revs):
644 646 single(cset, seqno, fp)
645 647
646 648 def diffstat(patchlines):
647 649 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
648 650 try:
649 651 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
650 652 try:
651 653 for line in patchlines: print >> p.tochild, line
652 654 p.tochild.close()
653 655 if p.wait(): return
654 656 fp = os.fdopen(fd, 'r')
655 657 stat = []
656 658 for line in fp: stat.append(line.lstrip())
657 659 last = stat.pop()
658 660 stat.insert(0, last)
659 661 stat = ''.join(stat)
660 662 if stat.startswith('0 files'): raise ValueError
661 663 return stat
662 664 except: raise
663 665 finally:
664 666 try: os.unlink(name)
665 667 except: pass
@@ -1,129 +1,145
1 1 #!/bin/sh
2 2
3 3 hg init a
4 4 cd a
5 5
6 6 echo % new file
7 7 hg import -mnew - <<EOF
8 8 diff --git a/new b/new
9 9 new file mode 100644
10 10 index 0000000..7898192
11 11 --- /dev/null
12 12 +++ b/new
13 13 @@ -0,0 +1 @@
14 14 +a
15 15 EOF
16 16
17 17 echo % new empty file
18 18 hg import -mempty - <<EOF
19 19 diff --git a/empty b/empty
20 20 new file mode 100644
21 21 EOF
22 22 hg locate empty
23 23
24 24 echo % chmod +x
25 25 hg import -msetx - <<EOF
26 26 diff --git a/new b/new
27 27 old mode 100644
28 28 new mode 100755
29 29 EOF
30 30
31 31 test -x new || echo failed
32 32
33 33 echo % copy
34 34 hg import -mcopy - <<EOF
35 35 diff --git a/new b/copy
36 36 old mode 100755
37 37 new mode 100644
38 38 similarity index 100%
39 39 copy from new
40 40 copy to copy
41 41 diff --git a/new b/copyx
42 42 similarity index 100%
43 43 copy from new
44 44 copy to copyx
45 45 EOF
46 46
47 47 test -f copy -a ! -x copy || echo failed
48 48 test -x copyx || echo failed
49 49 cat copy
50 50 hg cat copy
51 51
52 52 echo % rename
53 53 hg import -mrename - <<EOF
54 54 diff --git a/copy b/rename
55 55 similarity index 100%
56 56 rename from copy
57 57 rename to rename
58 58 EOF
59 59
60 60 hg locate
61 61
62 62 echo % delete
63 63 hg import -mdelete - <<EOF
64 64 diff --git a/copyx b/copyx
65 65 deleted file mode 100755
66 66 index 7898192..0000000
67 67 --- a/copyx
68 68 +++ /dev/null
69 69 @@ -1 +0,0 @@
70 70 -a
71 71 EOF
72 72
73 73 hg locate
74 74 test -f copyx && echo failed || true
75 75
76 76 echo % regular diff
77 77 hg import -mregular - <<EOF
78 78 diff --git a/rename b/rename
79 79 index 7898192..72e1fe3 100644
80 80 --- a/rename
81 81 +++ b/rename
82 82 @@ -1 +1,5 @@
83 83 a
84 84 +a
85 85 +a
86 86 +a
87 87 +a
88 88 EOF
89 89
90 90 echo % copy and modify
91 91 hg import -mcopymod - <<EOF
92 92 diff --git a/rename b/copy2
93 93 similarity index 80%
94 94 copy from rename
95 95 copy to copy2
96 96 index 72e1fe3..b53c148 100644
97 97 --- a/rename
98 98 +++ b/copy2
99 99 @@ -1,5 +1,5 @@
100 100 a
101 101 a
102 102 -a
103 103 +b
104 104 a
105 105 a
106 106 EOF
107 107
108 108 hg cat copy2
109 109
110 110 echo % rename and modify
111 111 hg import -mrenamemod - <<EOF
112 112 diff --git a/copy2 b/rename2
113 113 similarity index 80%
114 114 rename from copy2
115 115 rename to rename2
116 116 index b53c148..8f81e29 100644
117 117 --- a/copy2
118 118 +++ b/rename2
119 119 @@ -1,5 +1,5 @@
120 120 a
121 121 a
122 122 b
123 123 -a
124 124 +c
125 125 a
126 126 EOF
127 127
128 128 hg locate copy2
129 129 hg cat rename2
130
131 echo % one file renamed multiple times
132 hg import -mmultirenames - <<EOF
133 diff --git a/rename2 b/rename3
134 rename from rename2
135 rename to rename3
136 diff --git a/rename2 b/rename3-2
137 rename from rename2
138 rename to rename3-2
139 EOF
140 hg log -vCr. --template '{rev} {files} / {file_copies%filecopy}\n'
141
142 hg locate rename2 rename3 rename3-2
143 hg cat rename3
144 echo
145 hg cat rename3-2
@@ -1,39 +1,56
1 1 % new file
2 2 applying patch from stdin
3 3 % new empty file
4 4 applying patch from stdin
5 5 empty
6 6 % chmod +x
7 7 applying patch from stdin
8 8 % copy
9 9 applying patch from stdin
10 10 a
11 11 a
12 12 % rename
13 13 applying patch from stdin
14 14 copyx
15 15 empty
16 16 new
17 17 rename
18 18 % delete
19 19 applying patch from stdin
20 20 empty
21 21 new
22 22 rename
23 23 % regular diff
24 24 applying patch from stdin
25 25 % copy and modify
26 26 applying patch from stdin
27 27 a
28 28 a
29 29 b
30 30 a
31 31 a
32 32 % rename and modify
33 33 applying patch from stdin
34 34 copy2: No such file or directory
35 35 a
36 36 a
37 37 b
38 38 c
39 39 a
40 % one file renamed multiple times
41 applying patch from stdin
42 9 rename2 rename3 rename3-2 / rename3 (rename2)rename3-2 (rename2)
43 rename2: No such file or directory
44 rename3
45 rename3-2
46 a
47 a
48 b
49 c
50 a
51
52 a
53 a
54 b
55 c
56 a
General Comments 0
You need to be logged in to leave comments. Login now