##// END OF EJS Templates
Don't generate git diff header for empty diffs
Brendan Cully -
r3329:319358e6 default
parent child Browse files
Show More
@@ -1,575 +1,576
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(), "cmdutil mdiff util")
12 12 demandload(globals(), '''cStringIO email.Parser errno os re shutil sys tempfile
13 13 popen2''')
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 try:
30 30 shutil.copyfile(abssrc, absdst)
31 31 shutil.copymode(abssrc, absdst)
32 32 except shutil.Error, inst:
33 33 raise util.Abort(str(inst))
34 34
35 35 # public functions
36 36
37 37 def extract(ui, fileobj):
38 38 '''extract patch from data read from fileobj.
39 39
40 40 patch can be normal patch or contained in email message.
41 41
42 42 return tuple (filename, message, user, date). any item in returned
43 43 tuple can be None. if filename is None, fileobj did not contain
44 44 patch. caller must unlink filename when done.'''
45 45
46 46 # attempt to detect the start of a patch
47 47 # (this heuristic is borrowed from quilt)
48 48 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
49 49 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
50 50 '(---|\*\*\*)[ \t])', re.MULTILINE)
51 51
52 52 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
53 53 tmpfp = os.fdopen(fd, 'w')
54 54 try:
55 55 hgpatch = False
56 56
57 57 msg = email.Parser.Parser().parse(fileobj)
58 58
59 59 message = msg['Subject']
60 60 user = msg['From']
61 61 # should try to parse msg['Date']
62 62 date = None
63 63
64 64 if message:
65 65 message = message.replace('\n\t', ' ')
66 66 ui.debug('Subject: %s\n' % message)
67 67 if user:
68 68 ui.debug('From: %s\n' % user)
69 69 diffs_seen = 0
70 70 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
71 71
72 72 for part in msg.walk():
73 73 content_type = part.get_content_type()
74 74 ui.debug('Content-Type: %s\n' % content_type)
75 75 if content_type not in ok_types:
76 76 continue
77 77 payload = part.get_payload(decode=True)
78 78 m = diffre.search(payload)
79 79 if m:
80 80 ui.debug(_('found patch at byte %d\n') % m.start(0))
81 81 diffs_seen += 1
82 82 cfp = cStringIO.StringIO()
83 83 if message:
84 84 cfp.write(message)
85 85 cfp.write('\n')
86 86 for line in payload[:m.start(0)].splitlines():
87 87 if line.startswith('# HG changeset patch'):
88 88 ui.debug(_('patch generated by hg export\n'))
89 89 hgpatch = True
90 90 # drop earlier commit message content
91 91 cfp.seek(0)
92 92 cfp.truncate()
93 93 elif hgpatch:
94 94 if line.startswith('# User '):
95 95 user = line[7:]
96 96 ui.debug('From: %s\n' % user)
97 97 elif line.startswith("# Date "):
98 98 date = line[7:]
99 99 if not line.startswith('# '):
100 100 cfp.write(line)
101 101 cfp.write('\n')
102 102 message = cfp.getvalue()
103 103 if tmpfp:
104 104 tmpfp.write(payload)
105 105 if not payload.endswith('\n'):
106 106 tmpfp.write('\n')
107 107 elif not diffs_seen and message and content_type == 'text/plain':
108 108 message += '\n' + payload
109 109 except:
110 110 tmpfp.close()
111 111 os.unlink(tmpname)
112 112 raise
113 113
114 114 tmpfp.close()
115 115 if not diffs_seen:
116 116 os.unlink(tmpname)
117 117 return None, message, user, date
118 118 return tmpname, message, user, date
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
132 132 # Filter patch for git information
133 133 gitre = re.compile('diff --git a/(.*) b/(.*)')
134 134 pf = file(patchname)
135 135 gp = None
136 136 gitpatches = []
137 137 # Can have a git patch with only metadata, causing patch to complain
138 138 dopatch = False
139 139
140 140 lineno = 0
141 141 for line in pf:
142 142 lineno += 1
143 143 if line.startswith('diff --git'):
144 144 m = gitre.match(line)
145 145 if m:
146 146 if gp:
147 147 gitpatches.append(gp)
148 148 src, dst = m.group(1,2)
149 149 gp = gitpatch(dst)
150 150 gp.lineno = lineno
151 151 elif gp:
152 152 if line.startswith('--- '):
153 153 if gp.op in ('COPY', 'RENAME'):
154 154 gp.copymod = True
155 155 dopatch = 'filter'
156 156 gitpatches.append(gp)
157 157 gp = None
158 158 if not dopatch:
159 159 dopatch = True
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 if gp:
179 179 gitpatches.append(gp)
180 180
181 181 if not gitpatches:
182 182 dopatch = True
183 183
184 184 return (dopatch, gitpatches)
185 185
186 186 def dogitpatch(patchname, gitpatches, cwd=None):
187 187 """Preprocess git patch so that vanilla patch can handle it"""
188 188 pf = file(patchname)
189 189 pfline = 1
190 190
191 191 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
192 192 tmpfp = os.fdopen(fd, 'w')
193 193
194 194 try:
195 195 for i in range(len(gitpatches)):
196 196 p = gitpatches[i]
197 197 if not p.copymod:
198 198 continue
199 199
200 200 copyfile(p.oldpath, p.path, basedir=cwd)
201 201
202 202 # rewrite patch hunk
203 203 while pfline < p.lineno:
204 204 tmpfp.write(pf.readline())
205 205 pfline += 1
206 206 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
207 207 line = pf.readline()
208 208 pfline += 1
209 209 while not line.startswith('--- a/'):
210 210 tmpfp.write(line)
211 211 line = pf.readline()
212 212 pfline += 1
213 213 tmpfp.write('--- a/%s\n' % p.path)
214 214
215 215 line = pf.readline()
216 216 while line:
217 217 tmpfp.write(line)
218 218 line = pf.readline()
219 219 except:
220 220 tmpfp.close()
221 221 os.unlink(patchname)
222 222 raise
223 223
224 224 tmpfp.close()
225 225 return patchname
226 226
227 227 def patch(patchname, ui, strip=1, cwd=None):
228 228 """apply the patch <patchname> to the working directory.
229 229 a list of patched files is returned"""
230 230
231 231 # helper function
232 232 def __patch(patchname):
233 233 """patch and updates the files and fuzz variables"""
234 234 files = {}
235 235 fuzz = False
236 236
237 237 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
238 238 'patch')
239 239 args = []
240 240 if cwd:
241 241 args.append('-d %s' % util.shellquote(cwd))
242 242 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
243 243 util.shellquote(patchname)))
244 244
245 245 for line in fp:
246 246 line = line.rstrip()
247 247 ui.note(line + '\n')
248 248 if line.startswith('patching file '):
249 249 pf = util.parse_patch_output(line)
250 250 printed_file = False
251 251 files.setdefault(pf, (None, None))
252 252 elif line.find('with fuzz') >= 0:
253 253 fuzz = True
254 254 if not printed_file:
255 255 ui.warn(pf + '\n')
256 256 printed_file = True
257 257 ui.warn(line + '\n')
258 258 elif line.find('saving rejects to file') >= 0:
259 259 ui.warn(line + '\n')
260 260 elif line.find('FAILED') >= 0:
261 261 if not printed_file:
262 262 ui.warn(pf + '\n')
263 263 printed_file = True
264 264 ui.warn(line + '\n')
265 265 code = fp.close()
266 266 if code:
267 267 raise util.Abort(_("patch command failed: %s") %
268 268 util.explain_exit(code)[0])
269 269 return files, fuzz
270 270
271 271 (dopatch, gitpatches) = readgitpatch(patchname)
272 272
273 273 if dopatch:
274 274 if dopatch == 'filter':
275 275 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
276 276 try:
277 277 files, fuzz = __patch(patchname)
278 278 finally:
279 279 if dopatch == 'filter':
280 280 os.unlink(patchname)
281 281 else:
282 282 files, fuzz = {}, False
283 283
284 284 for gp in gitpatches:
285 285 files[gp.path] = (gp.op, gp)
286 286
287 287 return (files, fuzz)
288 288
289 289 def diffopts(ui, opts={}):
290 290 return mdiff.diffopts(
291 291 text=opts.get('text'),
292 292 git=(opts.get('git') or
293 293 ui.configbool('diff', 'git', None)),
294 294 nodates=(opts.get('nodates') or
295 295 ui.configbool('diff', 'nodates', None)),
296 296 showfunc=(opts.get('show_function') or
297 297 ui.configbool('diff', 'showfunc', None)),
298 298 ignorews=(opts.get('ignore_all_space') or
299 299 ui.configbool('diff', 'ignorews', None)),
300 300 ignorewsamount=(opts.get('ignore_space_change') or
301 301 ui.configbool('diff', 'ignorewsamount', None)),
302 302 ignoreblanklines=(opts.get('ignore_blank_lines') or
303 303 ui.configbool('diff', 'ignoreblanklines', None)))
304 304
305 305 def updatedir(ui, repo, patches, wlock=None):
306 306 '''Update dirstate after patch application according to metadata'''
307 307 if not patches:
308 308 return
309 309 copies = []
310 310 removes = []
311 311 cfiles = patches.keys()
312 312 cwd = repo.getcwd()
313 313 if cwd:
314 314 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
315 315 for f in patches:
316 316 ctype, gp = patches[f]
317 317 if ctype == 'RENAME':
318 318 copies.append((gp.oldpath, gp.path, gp.copymod))
319 319 removes.append(gp.oldpath)
320 320 elif ctype == 'COPY':
321 321 copies.append((gp.oldpath, gp.path, gp.copymod))
322 322 elif ctype == 'DELETE':
323 323 removes.append(gp.path)
324 324 for src, dst, after in copies:
325 325 if not after:
326 326 copyfile(src, dst, repo.root)
327 327 repo.copy(src, dst, wlock=wlock)
328 328 if removes:
329 329 repo.remove(removes, True, wlock=wlock)
330 330 for f in patches:
331 331 ctype, gp = patches[f]
332 332 if gp and gp.mode:
333 333 x = gp.mode & 0100 != 0
334 334 dst = os.path.join(repo.root, gp.path)
335 335 util.set_exec(dst, x)
336 336 cmdutil.addremove(repo, cfiles, wlock=wlock)
337 337 files = patches.keys()
338 338 files.extend([r for r in removes if r not in files])
339 339 files.sort()
340 340
341 341 return files
342 342
343 343 def diff(repo, node1=None, node2=None, files=None, match=util.always,
344 344 fp=None, changes=None, opts=None):
345 345 '''print diff of changes to files between two nodes, or node and
346 346 working directory.
347 347
348 348 if node1 is None, use first dirstate parent instead.
349 349 if node2 is None, compare node1 with working directory.'''
350 350
351 351 if opts is None:
352 352 opts = mdiff.defaultopts
353 353 if fp is None:
354 354 fp = repo.ui
355 355
356 356 if not node1:
357 357 node1 = repo.dirstate.parents()[0]
358 358
359 359 clcache = {}
360 360 def getchangelog(n):
361 361 if n not in clcache:
362 362 clcache[n] = repo.changelog.read(n)
363 363 return clcache[n]
364 364 mcache = {}
365 365 def getmanifest(n):
366 366 if n not in mcache:
367 367 mcache[n] = repo.manifest.read(n)
368 368 return mcache[n]
369 369 fcache = {}
370 370 def getfile(f):
371 371 if f not in fcache:
372 372 fcache[f] = repo.file(f)
373 373 return fcache[f]
374 374
375 375 # reading the data for node1 early allows it to play nicely
376 376 # with repo.status and the revlog cache.
377 377 change = getchangelog(node1)
378 378 mmap = getmanifest(change[0])
379 379 date1 = util.datestr(change[2])
380 380
381 381 if not changes:
382 382 changes = repo.status(node1, node2, files, match=match)[:5]
383 383 modified, added, removed, deleted, unknown = changes
384 384 if files:
385 385 def filterfiles(filters):
386 386 l = [x for x in filters if x in files]
387 387
388 388 for t in files:
389 389 if not t.endswith("/"):
390 390 t += "/"
391 391 l += [x for x in filters if x.startswith(t)]
392 392 return l
393 393
394 394 modified, added, removed = map(filterfiles, (modified, added, removed))
395 395
396 396 if not modified and not added and not removed:
397 397 return
398 398
399 399 def renamedbetween(f, n1, n2):
400 400 r1, r2 = map(repo.changelog.rev, (n1, n2))
401 401 src = None
402 402 while r2 > r1:
403 403 cl = getchangelog(n2)[0]
404 404 m = getmanifest(cl)
405 405 try:
406 406 src = getfile(f).renamed(m[f])
407 407 except KeyError:
408 408 return None
409 409 if src:
410 410 f = src[0]
411 411 n2 = repo.changelog.parents(n2)[0]
412 412 r2 = repo.changelog.rev(n2)
413 413 return src
414 414
415 415 if node2:
416 416 change = getchangelog(node2)
417 417 mmap2 = getmanifest(change[0])
418 418 _date2 = util.datestr(change[2])
419 419 def date2(f):
420 420 return _date2
421 421 def read(f):
422 422 return getfile(f).read(mmap2[f])
423 423 def renamed(f):
424 424 return renamedbetween(f, node1, node2)
425 425 else:
426 426 tz = util.makedate()[1]
427 427 _date2 = util.datestr()
428 428 def date2(f):
429 429 try:
430 430 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
431 431 except OSError, err:
432 432 if err.errno != errno.ENOENT: raise
433 433 return _date2
434 434 def read(f):
435 435 return repo.wread(f)
436 436 def renamed(f):
437 437 src = repo.dirstate.copied(f)
438 438 parent = repo.dirstate.parents()[0]
439 439 if src:
440 440 f = src[0]
441 441 of = renamedbetween(f, node1, parent)
442 442 if of:
443 443 return of
444 444 elif src:
445 445 cl = getchangelog(parent)[0]
446 446 return (src, getmanifest(cl)[src])
447 447 else:
448 448 return None
449 449
450 450 if repo.ui.quiet:
451 451 r = None
452 452 else:
453 453 hexfunc = repo.ui.verbose and hex or short
454 454 r = [hexfunc(node) for node in [node1, node2] if node]
455 455
456 456 if opts.git:
457 457 copied = {}
458 458 for f in added:
459 459 src = renamed(f)
460 460 if src:
461 461 copied[f] = src
462 462 srcs = [x[1][0] for x in copied.items()]
463 463
464 464 all = modified + added + removed
465 465 all.sort()
466 466 for f in all:
467 467 to = None
468 468 tn = None
469 469 dodiff = True
470 header = []
470 471 if f in mmap:
471 472 to = getfile(f).read(mmap[f])
472 473 if f not in removed:
473 474 tn = read(f)
474 475 if opts.git:
475 476 def gitmode(x):
476 477 return x and '100755' or '100644'
477 478 def addmodehdr(header, omode, nmode):
478 479 if omode != nmode:
479 480 header.append('old mode %s\n' % omode)
480 481 header.append('new mode %s\n' % nmode)
481 482
482 483 a, b = f, f
483 header = []
484 484 if f in added:
485 485 if node2:
486 486 mode = gitmode(mmap2.execf(f))
487 487 else:
488 488 mode = gitmode(util.is_exec(repo.wjoin(f), None))
489 489 if f in copied:
490 490 a, arev = copied[f]
491 491 omode = gitmode(mmap.execf(a))
492 492 addmodehdr(header, omode, mode)
493 493 op = a in removed and 'rename' or 'copy'
494 494 header.append('%s from %s\n' % (op, a))
495 495 header.append('%s to %s\n' % (op, f))
496 496 to = getfile(a).read(arev)
497 497 else:
498 498 header.append('new file mode %s\n' % mode)
499 499 elif f in removed:
500 500 if f in srcs:
501 501 dodiff = False
502 502 else:
503 503 mode = gitmode(mmap.execf(f))
504 504 header.append('deleted file mode %s\n' % mode)
505 505 else:
506 506 omode = gitmode(mmap.execf(f))
507 507 if node2:
508 508 nmode = gitmode(mmap2.execf(f))
509 509 else:
510 510 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
511 511 addmodehdr(header, omode, nmode)
512 512 r = None
513 if dodiff:
514 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
513 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
514 if dodiff:
515 text = mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts)
516 if text or len(header) > 1:
515 517 fp.write(''.join(header))
516 if dodiff:
517 fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts))
518 fp.write(text)
518 519
519 520 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
520 521 opts=None):
521 522 '''export changesets as hg patches.'''
522 523
523 524 total = len(revs)
524 525 revwidth = max(map(len, revs))
525 526
526 527 def single(node, seqno, fp):
527 528 parents = [p for p in repo.changelog.parents(node) if p != nullid]
528 529 if switch_parent:
529 530 parents.reverse()
530 531 prev = (parents and parents[0]) or nullid
531 532 change = repo.changelog.read(node)
532 533
533 534 if not fp:
534 535 fp = cmdutil.make_file(repo, template, node, total=total,
535 536 seqno=seqno, revwidth=revwidth)
536 537 if fp not in (sys.stdout, repo.ui):
537 538 repo.ui.note("%s\n" % fp.name)
538 539
539 540 fp.write("# HG changeset patch\n")
540 541 fp.write("# User %s\n" % change[1])
541 542 fp.write("# Date %d %d\n" % change[2])
542 543 fp.write("# Node ID %s\n" % hex(node))
543 544 fp.write("# Parent %s\n" % hex(prev))
544 545 if len(parents) > 1:
545 546 fp.write("# Parent %s\n" % hex(parents[1]))
546 547 fp.write(change[4].rstrip())
547 548 fp.write("\n\n")
548 549
549 550 diff(repo, prev, node, fp=fp, opts=opts)
550 551 if fp not in (sys.stdout, repo.ui):
551 552 fp.close()
552 553
553 554 for seqno, cset in enumerate(revs):
554 555 single(cset, seqno, fp)
555 556
556 557 def diffstat(patchlines):
557 558 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
558 559 try:
559 560 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
560 561 try:
561 562 for line in patchlines: print >> p.tochild, line
562 563 p.tochild.close()
563 564 if p.wait(): return
564 565 fp = os.fdopen(fd, 'r')
565 566 stat = []
566 567 for line in fp: stat.append(line.lstrip())
567 568 last = stat.pop()
568 569 stat.insert(0, last)
569 570 stat = ''.join(stat)
570 571 if stat.startswith('0 files'): raise ValueError
571 572 return stat
572 573 except: raise
573 574 finally:
574 575 try: os.unlink(name)
575 576 except: pass
General Comments 0
You need to be logged in to leave comments. Login now