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