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