##// END OF EJS Templates
patch: add a workingbackend dirstate layer on top of fsbackend...
Patrick Mezard -
r14370:17cea10c default
parent child Browse files
Show More
@@ -1,555 +1,554 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 gettext, _
11 11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
12 12 from mercurial import util
13 13 import copy, cStringIO, errno, os, re, shutil, tempfile
14 14
15 15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16 16
17 17 def scanpatch(fp):
18 18 """like patch.iterhunks, but yield different events
19 19
20 20 - ('file', [header_lines + fromfile + tofile])
21 21 - ('context', [context_lines])
22 22 - ('hunk', [hunk_lines])
23 23 - ('range', (-start,len, +start,len, diffp))
24 24 """
25 25 lr = patch.linereader(fp)
26 26
27 27 def scanwhile(first, p):
28 28 """scan lr while predicate holds"""
29 29 lines = [first]
30 30 while True:
31 31 line = lr.readline()
32 32 if not line:
33 33 break
34 34 if p(line):
35 35 lines.append(line)
36 36 else:
37 37 lr.push(line)
38 38 break
39 39 return lines
40 40
41 41 while True:
42 42 line = lr.readline()
43 43 if not line:
44 44 break
45 45 if line.startswith('diff --git a/') or line.startswith('diff -r '):
46 46 def notheader(line):
47 47 s = line.split(None, 1)
48 48 return not s or s[0] not in ('---', 'diff')
49 49 header = scanwhile(line, notheader)
50 50 fromfile = lr.readline()
51 51 if fromfile.startswith('---'):
52 52 tofile = lr.readline()
53 53 header += [fromfile, tofile]
54 54 else:
55 55 lr.push(fromfile)
56 56 yield 'file', header
57 57 elif line[0] == ' ':
58 58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
59 59 elif line[0] in '-+':
60 60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
61 61 else:
62 62 m = lines_re.match(line)
63 63 if m:
64 64 yield 'range', m.groups()
65 65 else:
66 66 raise patch.PatchError('unknown patch content: %r' % line)
67 67
68 68 class header(object):
69 69 """patch header
70 70
71 71 XXX shoudn't we move this to mercurial/patch.py ?
72 72 """
73 73 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
74 74 diff_re = re.compile('diff -r .* (.*)$')
75 75 allhunks_re = re.compile('(?:index|new file|deleted file) ')
76 76 pretty_re = re.compile('(?:new file|deleted file) ')
77 77 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
78 78
79 79 def __init__(self, header):
80 80 self.header = header
81 81 self.hunks = []
82 82
83 83 def binary(self):
84 84 return util.any(h.startswith('index ') for h in self.header)
85 85
86 86 def pretty(self, fp):
87 87 for h in self.header:
88 88 if h.startswith('index '):
89 89 fp.write(_('this modifies a binary file (all or nothing)\n'))
90 90 break
91 91 if self.pretty_re.match(h):
92 92 fp.write(h)
93 93 if self.binary():
94 94 fp.write(_('this is a binary file\n'))
95 95 break
96 96 if h.startswith('---'):
97 97 fp.write(_('%d hunks, %d lines changed\n') %
98 98 (len(self.hunks),
99 99 sum([max(h.added, h.removed) for h in self.hunks])))
100 100 break
101 101 fp.write(h)
102 102
103 103 def write(self, fp):
104 104 fp.write(''.join(self.header))
105 105
106 106 def allhunks(self):
107 107 return util.any(self.allhunks_re.match(h) for h in self.header)
108 108
109 109 def files(self):
110 110 match = self.diffgit_re.match(self.header[0])
111 111 if match:
112 112 fromfile, tofile = match.groups()
113 113 if fromfile == tofile:
114 114 return [fromfile]
115 115 return [fromfile, tofile]
116 116 else:
117 117 return self.diff_re.match(self.header[0]).groups()
118 118
119 119 def filename(self):
120 120 return self.files()[-1]
121 121
122 122 def __repr__(self):
123 123 return '<header %s>' % (' '.join(map(repr, self.files())))
124 124
125 125 def special(self):
126 126 return util.any(self.special_re.match(h) for h in self.header)
127 127
128 128 def countchanges(hunk):
129 129 """hunk -> (n+,n-)"""
130 130 add = len([h for h in hunk if h[0] == '+'])
131 131 rem = len([h for h in hunk if h[0] == '-'])
132 132 return add, rem
133 133
134 134 class hunk(object):
135 135 """patch hunk
136 136
137 137 XXX shouldn't we merge this with patch.hunk ?
138 138 """
139 139 maxcontext = 3
140 140
141 141 def __init__(self, header, fromline, toline, proc, before, hunk, after):
142 142 def trimcontext(number, lines):
143 143 delta = len(lines) - self.maxcontext
144 144 if False and delta > 0:
145 145 return number + delta, lines[:self.maxcontext]
146 146 return number, lines
147 147
148 148 self.header = header
149 149 self.fromline, self.before = trimcontext(fromline, before)
150 150 self.toline, self.after = trimcontext(toline, after)
151 151 self.proc = proc
152 152 self.hunk = hunk
153 153 self.added, self.removed = countchanges(self.hunk)
154 154
155 155 def write(self, fp):
156 156 delta = len(self.before) + len(self.after)
157 157 if self.after and self.after[-1] == '\\ No newline at end of file\n':
158 158 delta -= 1
159 159 fromlen = delta + self.removed
160 160 tolen = delta + self.added
161 161 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
162 162 (self.fromline, fromlen, self.toline, tolen,
163 163 self.proc and (' ' + self.proc)))
164 164 fp.write(''.join(self.before + self.hunk + self.after))
165 165
166 166 pretty = write
167 167
168 168 def filename(self):
169 169 return self.header.filename()
170 170
171 171 def __repr__(self):
172 172 return '<hunk %r@%d>' % (self.filename(), self.fromline)
173 173
174 174 def parsepatch(fp):
175 175 """patch -> [] of headers -> [] of hunks """
176 176 class parser(object):
177 177 """patch parsing state machine"""
178 178 def __init__(self):
179 179 self.fromline = 0
180 180 self.toline = 0
181 181 self.proc = ''
182 182 self.header = None
183 183 self.context = []
184 184 self.before = []
185 185 self.hunk = []
186 186 self.headers = []
187 187
188 188 def addrange(self, limits):
189 189 fromstart, fromend, tostart, toend, proc = limits
190 190 self.fromline = int(fromstart)
191 191 self.toline = int(tostart)
192 192 self.proc = proc
193 193
194 194 def addcontext(self, context):
195 195 if self.hunk:
196 196 h = hunk(self.header, self.fromline, self.toline, self.proc,
197 197 self.before, self.hunk, context)
198 198 self.header.hunks.append(h)
199 199 self.fromline += len(self.before) + h.removed
200 200 self.toline += len(self.before) + h.added
201 201 self.before = []
202 202 self.hunk = []
203 203 self.proc = ''
204 204 self.context = context
205 205
206 206 def addhunk(self, hunk):
207 207 if self.context:
208 208 self.before = self.context
209 209 self.context = []
210 210 self.hunk = hunk
211 211
212 212 def newfile(self, hdr):
213 213 self.addcontext([])
214 214 h = header(hdr)
215 215 self.headers.append(h)
216 216 self.header = h
217 217
218 218 def finished(self):
219 219 self.addcontext([])
220 220 return self.headers
221 221
222 222 transitions = {
223 223 'file': {'context': addcontext,
224 224 'file': newfile,
225 225 'hunk': addhunk,
226 226 'range': addrange},
227 227 'context': {'file': newfile,
228 228 'hunk': addhunk,
229 229 'range': addrange},
230 230 'hunk': {'context': addcontext,
231 231 'file': newfile,
232 232 'range': addrange},
233 233 'range': {'context': addcontext,
234 234 'hunk': addhunk},
235 235 }
236 236
237 237 p = parser()
238 238
239 239 state = 'context'
240 240 for newstate, data in scanpatch(fp):
241 241 try:
242 242 p.transitions[state][newstate](p, data)
243 243 except KeyError:
244 244 raise patch.PatchError('unhandled transition: %s -> %s' %
245 245 (state, newstate))
246 246 state = newstate
247 247 return p.finished()
248 248
249 249 def filterpatch(ui, headers):
250 250 """Interactively filter patch chunks into applied-only chunks"""
251 251
252 252 def prompt(skipfile, skipall, query):
253 253 """prompt query, and process base inputs
254 254
255 255 - y/n for the rest of file
256 256 - y/n for the rest
257 257 - ? (help)
258 258 - q (quit)
259 259
260 260 Return True/False and possibly updated skipfile and skipall.
261 261 """
262 262 if skipall is not None:
263 263 return skipall, skipfile, skipall
264 264 if skipfile is not None:
265 265 return skipfile, skipfile, skipall
266 266 while True:
267 267 resps = _('[Ynsfdaq?]')
268 268 choices = (_('&Yes, record this change'),
269 269 _('&No, skip this change'),
270 270 _('&Skip remaining changes to this file'),
271 271 _('Record remaining changes to this &file'),
272 272 _('&Done, skip remaining changes and files'),
273 273 _('Record &all changes to all remaining files'),
274 274 _('&Quit, recording no changes'),
275 275 _('&?'))
276 276 r = ui.promptchoice("%s %s" % (query, resps), choices)
277 277 ui.write("\n")
278 278 if r == 7: # ?
279 279 doc = gettext(record.__doc__)
280 280 c = doc.find('::') + 2
281 281 for l in doc[c:].splitlines():
282 282 if l.startswith(' '):
283 283 ui.write(l.strip(), '\n')
284 284 continue
285 285 elif r == 0: # yes
286 286 ret = True
287 287 elif r == 1: # no
288 288 ret = False
289 289 elif r == 2: # Skip
290 290 ret = skipfile = False
291 291 elif r == 3: # file (Record remaining)
292 292 ret = skipfile = True
293 293 elif r == 4: # done, skip remaining
294 294 ret = skipall = False
295 295 elif r == 5: # all
296 296 ret = skipall = True
297 297 elif r == 6: # quit
298 298 raise util.Abort(_('user quit'))
299 299 return ret, skipfile, skipall
300 300
301 301 seen = set()
302 302 applied = {} # 'filename' -> [] of chunks
303 303 skipfile, skipall = None, None
304 304 pos, total = 1, sum(len(h.hunks) for h in headers)
305 305 for h in headers:
306 306 pos += len(h.hunks)
307 307 skipfile = None
308 308 fixoffset = 0
309 309 hdr = ''.join(h.header)
310 310 if hdr in seen:
311 311 continue
312 312 seen.add(hdr)
313 313 if skipall is None:
314 314 h.pretty(ui)
315 315 msg = (_('examine changes to %s?') %
316 316 _(' and ').join(map(repr, h.files())))
317 317 r, skipfile, skipall = prompt(skipfile, skipall, msg)
318 318 if not r:
319 319 continue
320 320 applied[h.filename()] = [h]
321 321 if h.allhunks():
322 322 applied[h.filename()] += h.hunks
323 323 continue
324 324 for i, chunk in enumerate(h.hunks):
325 325 if skipfile is None and skipall is None:
326 326 chunk.pretty(ui)
327 327 if total == 1:
328 328 msg = _('record this change to %r?') % chunk.filename()
329 329 else:
330 330 idx = pos - len(h.hunks) + i
331 331 msg = _('record change %d/%d to %r?') % (idx, total,
332 332 chunk.filename())
333 333 r, skipfile, skipall = prompt(skipfile, skipall, msg)
334 334 if r:
335 335 if fixoffset:
336 336 chunk = copy.copy(chunk)
337 337 chunk.toline += fixoffset
338 338 applied[chunk.filename()].append(chunk)
339 339 else:
340 340 fixoffset += chunk.removed - chunk.added
341 341 return sum([h for h in applied.itervalues()
342 342 if h[0].special() or len(h) > 1], [])
343 343
344 344 def record(ui, repo, *pats, **opts):
345 345 '''interactively select changes to commit
346 346
347 347 If a list of files is omitted, all changes reported by :hg:`status`
348 348 will be candidates for recording.
349 349
350 350 See :hg:`help dates` for a list of formats valid for -d/--date.
351 351
352 352 You will be prompted for whether to record changes to each
353 353 modified file, and for files with multiple changes, for each
354 354 change to use. For each query, the following responses are
355 355 possible::
356 356
357 357 y - record this change
358 358 n - skip this change
359 359
360 360 s - skip remaining changes to this file
361 361 f - record remaining changes to this file
362 362
363 363 d - done, skip remaining changes and files
364 364 a - record all changes to all remaining files
365 365 q - quit, recording no changes
366 366
367 367 ? - display help
368 368
369 369 This command is not available when committing a merge.'''
370 370
371 371 dorecord(ui, repo, commands.commit, *pats, **opts)
372 372
373 373
374 374 def qrecord(ui, repo, patch, *pats, **opts):
375 375 '''interactively record a new patch
376 376
377 377 See :hg:`help qnew` & :hg:`help record` for more information and
378 378 usage.
379 379 '''
380 380
381 381 try:
382 382 mq = extensions.find('mq')
383 383 except KeyError:
384 384 raise util.Abort(_("'mq' extension not loaded"))
385 385
386 386 def committomq(ui, repo, *pats, **opts):
387 387 mq.new(ui, repo, patch, *pats, **opts)
388 388
389 389 dorecord(ui, repo, committomq, *pats, **opts)
390 390
391 391
392 392 def dorecord(ui, repo, commitfunc, *pats, **opts):
393 393 if not ui.interactive():
394 394 raise util.Abort(_('running non-interactively, use commit instead'))
395 395
396 396 def recordfunc(ui, repo, message, match, opts):
397 397 """This is generic record driver.
398 398
399 399 Its job is to interactively filter local changes, and
400 400 accordingly prepare working directory into a state in which the
401 401 job can be delegated to a non-interactive commit command such as
402 402 'commit' or 'qrefresh'.
403 403
404 404 After the actual job is done by non-interactive command, the
405 405 working directory is restored to its original state.
406 406
407 407 In the end we'll record interesting changes, and everything else
408 408 will be left in place, so the user can continue working.
409 409 """
410 410
411 411 merge = len(repo[None].parents()) > 1
412 412 if merge:
413 413 raise util.Abort(_('cannot partially commit a merge '
414 414 '(use "hg commit" instead)'))
415 415
416 416 changes = repo.status(match=match)[:3]
417 417 diffopts = mdiff.diffopts(git=True, nodates=True)
418 418 chunks = patch.diff(repo, changes=changes, opts=diffopts)
419 419 fp = cStringIO.StringIO()
420 420 fp.write(''.join(chunks))
421 421 fp.seek(0)
422 422
423 423 # 1. filter patch, so we have intending-to apply subset of it
424 424 chunks = filterpatch(ui, parsepatch(fp))
425 425 del fp
426 426
427 427 contenders = set()
428 428 for h in chunks:
429 429 try:
430 430 contenders.update(set(h.files()))
431 431 except AttributeError:
432 432 pass
433 433
434 434 changed = changes[0] + changes[1] + changes[2]
435 435 newfiles = [f for f in changed if f in contenders]
436 436 if not newfiles:
437 437 ui.status(_('no changes to record\n'))
438 438 return 0
439 439
440 440 modified = set(changes[0])
441 441
442 442 # 2. backup changed files, so we can restore them in the end
443 443 backups = {}
444 444 backupdir = repo.join('record-backups')
445 445 try:
446 446 os.mkdir(backupdir)
447 447 except OSError, err:
448 448 if err.errno != errno.EEXIST:
449 449 raise
450 450 try:
451 451 # backup continues
452 452 for f in newfiles:
453 453 if f not in modified:
454 454 continue
455 455 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
456 456 dir=backupdir)
457 457 os.close(fd)
458 458 ui.debug('backup %r as %r\n' % (f, tmpname))
459 459 util.copyfile(repo.wjoin(f), tmpname)
460 460 shutil.copystat(repo.wjoin(f), tmpname)
461 461 backups[f] = tmpname
462 462
463 463 fp = cStringIO.StringIO()
464 464 for c in chunks:
465 465 if c.filename() in backups:
466 466 c.write(fp)
467 467 dopatch = fp.tell()
468 468 fp.seek(0)
469 469
470 470 # 3a. apply filtered patch to clean repo (clean)
471 471 if backups:
472 472 hg.revert(repo, repo.dirstate.p1(),
473 473 lambda key: key in backups)
474 474
475 475 # 3b. (apply)
476 476 if dopatch:
477 477 try:
478 478 ui.debug('applying patch\n')
479 479 ui.debug(fp.getvalue())
480 patch.internalpatch(ui, repo, fp, 1, repo.root,
481 eolmode=None)
480 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
482 481 except patch.PatchError, err:
483 482 raise util.Abort(str(err))
484 483 del fp
485 484
486 485 # 4. We prepared working directory according to filtered
487 486 # patch. Now is the time to delegate the job to
488 487 # commit/qrefresh or the like!
489 488
490 489 # it is important to first chdir to repo root -- we'll call
491 490 # a highlevel command with list of pathnames relative to
492 491 # repo root
493 492 cwd = os.getcwd()
494 493 os.chdir(repo.root)
495 494 try:
496 495 commitfunc(ui, repo, *newfiles, **opts)
497 496 finally:
498 497 os.chdir(cwd)
499 498
500 499 return 0
501 500 finally:
502 501 # 5. finally restore backed-up files
503 502 try:
504 503 for realname, tmpname in backups.iteritems():
505 504 ui.debug('restoring %r to %r\n' % (tmpname, realname))
506 505 util.copyfile(tmpname, repo.wjoin(realname))
507 506 # Our calls to copystat() here and above are a
508 507 # hack to trick any editors that have f open that
509 508 # we haven't modified them.
510 509 #
511 510 # Also note that this racy as an editor could
512 511 # notice the file's mtime before we've finished
513 512 # writing it.
514 513 shutil.copystat(tmpname, repo.wjoin(realname))
515 514 os.unlink(tmpname)
516 515 os.rmdir(backupdir)
517 516 except OSError:
518 517 pass
519 518
520 519 # wrap ui.write so diff output can be labeled/colorized
521 520 def wrapwrite(orig, *args, **kw):
522 521 label = kw.pop('label', '')
523 522 for chunk, l in patch.difflabel(lambda: args):
524 523 orig(chunk, label=label + l)
525 524 oldwrite = ui.write
526 525 extensions.wrapfunction(ui, 'write', wrapwrite)
527 526 try:
528 527 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
529 528 finally:
530 529 ui.write = oldwrite
531 530
532 531 cmdtable = {
533 532 "record":
534 533 (record, commands.table['^commit|ci'][1], # same options as commit
535 534 _('hg record [OPTION]... [FILE]...')),
536 535 "qrecord":
537 536 (qrecord, {}, # placeholder until mq is available
538 537 _('hg qrecord [OPTION]... PATCH [FILE]...')),
539 538 }
540 539
541 540
542 541 def uisetup(ui):
543 542 try:
544 543 mq = extensions.find('mq')
545 544 except KeyError:
546 545 return
547 546
548 547 qcmdtable = {
549 548 "qrecord":
550 549 (qrecord, mq.cmdtable['^qnew'][1], # same options as qnew
551 550 _('hg qrecord [OPTION]... PATCH [FILE]...')),
552 551 }
553 552
554 553 cmdtable.update(qcmdtable)
555 554
@@ -1,1768 +1,1807 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.Parser, os, errno, re
10 10 import tempfile, zlib
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
15 15
16 16 gitre = re.compile('diff --git a/(.*) b/(.*)')
17 17
18 18 class PatchError(Exception):
19 19 pass
20 20
21 21
22 22 # public functions
23 23
24 24 def split(stream):
25 25 '''return an iterator of individual patches from a stream'''
26 26 def isheader(line, inheader):
27 27 if inheader and line[0] in (' ', '\t'):
28 28 # continuation
29 29 return True
30 30 if line[0] in (' ', '-', '+'):
31 31 # diff line - don't check for header pattern in there
32 32 return False
33 33 l = line.split(': ', 1)
34 34 return len(l) == 2 and ' ' not in l[0]
35 35
36 36 def chunk(lines):
37 37 return cStringIO.StringIO(''.join(lines))
38 38
39 39 def hgsplit(stream, cur):
40 40 inheader = True
41 41
42 42 for line in stream:
43 43 if not line.strip():
44 44 inheader = False
45 45 if not inheader and line.startswith('# HG changeset patch'):
46 46 yield chunk(cur)
47 47 cur = []
48 48 inheader = True
49 49
50 50 cur.append(line)
51 51
52 52 if cur:
53 53 yield chunk(cur)
54 54
55 55 def mboxsplit(stream, cur):
56 56 for line in stream:
57 57 if line.startswith('From '):
58 58 for c in split(chunk(cur[1:])):
59 59 yield c
60 60 cur = []
61 61
62 62 cur.append(line)
63 63
64 64 if cur:
65 65 for c in split(chunk(cur[1:])):
66 66 yield c
67 67
68 68 def mimesplit(stream, cur):
69 69 def msgfp(m):
70 70 fp = cStringIO.StringIO()
71 71 g = email.Generator.Generator(fp, mangle_from_=False)
72 72 g.flatten(m)
73 73 fp.seek(0)
74 74 return fp
75 75
76 76 for line in stream:
77 77 cur.append(line)
78 78 c = chunk(cur)
79 79
80 80 m = email.Parser.Parser().parse(c)
81 81 if not m.is_multipart():
82 82 yield msgfp(m)
83 83 else:
84 84 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
85 85 for part in m.walk():
86 86 ct = part.get_content_type()
87 87 if ct not in ok_types:
88 88 continue
89 89 yield msgfp(part)
90 90
91 91 def headersplit(stream, cur):
92 92 inheader = False
93 93
94 94 for line in stream:
95 95 if not inheader and isheader(line, inheader):
96 96 yield chunk(cur)
97 97 cur = []
98 98 inheader = True
99 99 if inheader and not isheader(line, inheader):
100 100 inheader = False
101 101
102 102 cur.append(line)
103 103
104 104 if cur:
105 105 yield chunk(cur)
106 106
107 107 def remainder(cur):
108 108 yield chunk(cur)
109 109
110 110 class fiter(object):
111 111 def __init__(self, fp):
112 112 self.fp = fp
113 113
114 114 def __iter__(self):
115 115 return self
116 116
117 117 def next(self):
118 118 l = self.fp.readline()
119 119 if not l:
120 120 raise StopIteration
121 121 return l
122 122
123 123 inheader = False
124 124 cur = []
125 125
126 126 mimeheaders = ['content-type']
127 127
128 128 if not hasattr(stream, 'next'):
129 129 # http responses, for example, have readline but not next
130 130 stream = fiter(stream)
131 131
132 132 for line in stream:
133 133 cur.append(line)
134 134 if line.startswith('# HG changeset patch'):
135 135 return hgsplit(stream, cur)
136 136 elif line.startswith('From '):
137 137 return mboxsplit(stream, cur)
138 138 elif isheader(line, inheader):
139 139 inheader = True
140 140 if line.split(':', 1)[0].lower() in mimeheaders:
141 141 # let email parser handle this
142 142 return mimesplit(stream, cur)
143 143 elif line.startswith('--- ') and inheader:
144 144 # No evil headers seen by diff start, split by hand
145 145 return headersplit(stream, cur)
146 146 # Not enough info, keep reading
147 147
148 148 # if we are here, we have a very plain patch
149 149 return remainder(cur)
150 150
151 151 def extract(ui, fileobj):
152 152 '''extract patch from data read from fileobj.
153 153
154 154 patch can be a normal patch or contained in an email message.
155 155
156 156 return tuple (filename, message, user, date, branch, node, p1, p2).
157 157 Any item in the returned tuple can be None. If filename is None,
158 158 fileobj did not contain a patch. Caller must unlink filename when done.'''
159 159
160 160 # attempt to detect the start of a patch
161 161 # (this heuristic is borrowed from quilt)
162 162 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
163 163 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
164 164 r'---[ \t].*?^\+\+\+[ \t]|'
165 165 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
166 166
167 167 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
168 168 tmpfp = os.fdopen(fd, 'w')
169 169 try:
170 170 msg = email.Parser.Parser().parse(fileobj)
171 171
172 172 subject = msg['Subject']
173 173 user = msg['From']
174 174 if not subject and not user:
175 175 # Not an email, restore parsed headers if any
176 176 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
177 177
178 178 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
179 179 # should try to parse msg['Date']
180 180 date = None
181 181 nodeid = None
182 182 branch = None
183 183 parents = []
184 184
185 185 if subject:
186 186 if subject.startswith('[PATCH'):
187 187 pend = subject.find(']')
188 188 if pend >= 0:
189 189 subject = subject[pend + 1:].lstrip()
190 190 subject = subject.replace('\n\t', ' ')
191 191 ui.debug('Subject: %s\n' % subject)
192 192 if user:
193 193 ui.debug('From: %s\n' % user)
194 194 diffs_seen = 0
195 195 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
196 196 message = ''
197 197 for part in msg.walk():
198 198 content_type = part.get_content_type()
199 199 ui.debug('Content-Type: %s\n' % content_type)
200 200 if content_type not in ok_types:
201 201 continue
202 202 payload = part.get_payload(decode=True)
203 203 m = diffre.search(payload)
204 204 if m:
205 205 hgpatch = False
206 206 hgpatchheader = False
207 207 ignoretext = False
208 208
209 209 ui.debug('found patch at byte %d\n' % m.start(0))
210 210 diffs_seen += 1
211 211 cfp = cStringIO.StringIO()
212 212 for line in payload[:m.start(0)].splitlines():
213 213 if line.startswith('# HG changeset patch') and not hgpatch:
214 214 ui.debug('patch generated by hg export\n')
215 215 hgpatch = True
216 216 hgpatchheader = True
217 217 # drop earlier commit message content
218 218 cfp.seek(0)
219 219 cfp.truncate()
220 220 subject = None
221 221 elif hgpatchheader:
222 222 if line.startswith('# User '):
223 223 user = line[7:]
224 224 ui.debug('From: %s\n' % user)
225 225 elif line.startswith("# Date "):
226 226 date = line[7:]
227 227 elif line.startswith("# Branch "):
228 228 branch = line[9:]
229 229 elif line.startswith("# Node ID "):
230 230 nodeid = line[10:]
231 231 elif line.startswith("# Parent "):
232 232 parents.append(line[10:])
233 233 elif not line.startswith("# "):
234 234 hgpatchheader = False
235 235 elif line == '---' and gitsendmail:
236 236 ignoretext = True
237 237 if not hgpatchheader and not ignoretext:
238 238 cfp.write(line)
239 239 cfp.write('\n')
240 240 message = cfp.getvalue()
241 241 if tmpfp:
242 242 tmpfp.write(payload)
243 243 if not payload.endswith('\n'):
244 244 tmpfp.write('\n')
245 245 elif not diffs_seen and message and content_type == 'text/plain':
246 246 message += '\n' + payload
247 247 except:
248 248 tmpfp.close()
249 249 os.unlink(tmpname)
250 250 raise
251 251
252 252 if subject and not message.startswith(subject):
253 253 message = '%s\n%s' % (subject, message)
254 254 tmpfp.close()
255 255 if not diffs_seen:
256 256 os.unlink(tmpname)
257 257 return None, message, user, date, branch, None, None, None
258 258 p1 = parents and parents.pop(0) or None
259 259 p2 = parents and parents.pop(0) or None
260 260 return tmpname, message, user, date, branch, nodeid, p1, p2
261 261
262 262 class patchmeta(object):
263 263 """Patched file metadata
264 264
265 265 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
266 266 or COPY. 'path' is patched file path. 'oldpath' is set to the
267 267 origin file when 'op' is either COPY or RENAME, None otherwise. If
268 268 file mode is changed, 'mode' is a tuple (islink, isexec) where
269 269 'islink' is True if the file is a symlink and 'isexec' is True if
270 270 the file is executable. Otherwise, 'mode' is None.
271 271 """
272 272 def __init__(self, path):
273 273 self.path = path
274 274 self.oldpath = None
275 275 self.mode = None
276 276 self.op = 'MODIFY'
277 277 self.binary = False
278 278
279 279 def setmode(self, mode):
280 280 islink = mode & 020000
281 281 isexec = mode & 0100
282 282 self.mode = (islink, isexec)
283 283
284 284 def __repr__(self):
285 285 return "<patchmeta %s %r>" % (self.op, self.path)
286 286
287 287 def readgitpatch(lr):
288 288 """extract git-style metadata about patches from <patchname>"""
289 289
290 290 # Filter patch for git information
291 291 gp = None
292 292 gitpatches = []
293 293 for line in lr:
294 294 line = line.rstrip(' \r\n')
295 295 if line.startswith('diff --git'):
296 296 m = gitre.match(line)
297 297 if m:
298 298 if gp:
299 299 gitpatches.append(gp)
300 300 dst = m.group(2)
301 301 gp = patchmeta(dst)
302 302 elif gp:
303 303 if line.startswith('--- '):
304 304 gitpatches.append(gp)
305 305 gp = None
306 306 continue
307 307 if line.startswith('rename from '):
308 308 gp.op = 'RENAME'
309 309 gp.oldpath = line[12:]
310 310 elif line.startswith('rename to '):
311 311 gp.path = line[10:]
312 312 elif line.startswith('copy from '):
313 313 gp.op = 'COPY'
314 314 gp.oldpath = line[10:]
315 315 elif line.startswith('copy to '):
316 316 gp.path = line[8:]
317 317 elif line.startswith('deleted file'):
318 318 gp.op = 'DELETE'
319 319 elif line.startswith('new file mode '):
320 320 gp.op = 'ADD'
321 321 gp.setmode(int(line[-6:], 8))
322 322 elif line.startswith('new mode '):
323 323 gp.setmode(int(line[-6:], 8))
324 324 elif line.startswith('GIT binary patch'):
325 325 gp.binary = True
326 326 if gp:
327 327 gitpatches.append(gp)
328 328
329 329 return gitpatches
330 330
331 331 class linereader(object):
332 332 # simple class to allow pushing lines back into the input stream
333 333 def __init__(self, fp, textmode=False):
334 334 self.fp = fp
335 335 self.buf = []
336 336 self.textmode = textmode
337 337 self.eol = None
338 338
339 339 def push(self, line):
340 340 if line is not None:
341 341 self.buf.append(line)
342 342
343 343 def readline(self):
344 344 if self.buf:
345 345 l = self.buf[0]
346 346 del self.buf[0]
347 347 return l
348 348 l = self.fp.readline()
349 349 if not self.eol:
350 350 if l.endswith('\r\n'):
351 351 self.eol = '\r\n'
352 352 elif l.endswith('\n'):
353 353 self.eol = '\n'
354 354 if self.textmode and l.endswith('\r\n'):
355 355 l = l[:-2] + '\n'
356 356 return l
357 357
358 358 def __iter__(self):
359 359 while 1:
360 360 l = self.readline()
361 361 if not l:
362 362 break
363 363 yield l
364 364
365 365 class abstractbackend(object):
366 366 def __init__(self, ui):
367 367 self.ui = ui
368 368
369 369 def readlines(self, fname):
370 370 """Return target file lines, or its content as a single line
371 371 for symlinks.
372 372 """
373 373 raise NotImplementedError
374 374
375 375 def writelines(self, fname, lines, mode):
376 376 """Write lines to target file. mode is a (islink, isexec)
377 377 tuple, or None if there is no mode information.
378 378 """
379 379 raise NotImplementedError
380 380
381 381 def unlink(self, fname):
382 382 """Unlink target file."""
383 383 raise NotImplementedError
384 384
385 385 def writerej(self, fname, failed, total, lines):
386 386 """Write rejected lines for fname. total is the number of hunks
387 387 which failed to apply and total the total number of hunks for this
388 388 files.
389 389 """
390 390 pass
391 391
392 392 def copy(self, src, dst):
393 393 """Copy src file into dst file. Create intermediate directories if
394 394 necessary. Files are specified relatively to the patching base
395 395 directory.
396 396 """
397 397 raise NotImplementedError
398 398
399 399 def exists(self, fname):
400 400 raise NotImplementedError
401 401
402 402 def setmode(self, fname, islink, isexec):
403 403 """Change target file mode."""
404 404 raise NotImplementedError
405 405
406 406 class fsbackend(abstractbackend):
407 407 def __init__(self, ui, basedir):
408 408 super(fsbackend, self).__init__(ui)
409 409 self.opener = scmutil.opener(basedir)
410 410
411 411 def _join(self, f):
412 412 return os.path.join(self.opener.base, f)
413 413
414 414 def readlines(self, fname):
415 415 if os.path.islink(self._join(fname)):
416 416 return [os.readlink(self._join(fname))]
417 417 fp = self.opener(fname, 'r')
418 418 try:
419 419 return list(fp)
420 420 finally:
421 421 fp.close()
422 422
423 423 def writelines(self, fname, lines, mode):
424 424 if not mode:
425 425 # Preserve mode information
426 426 isexec, islink = False, False
427 427 try:
428 428 isexec = os.lstat(self._join(fname)).st_mode & 0100 != 0
429 429 islink = os.path.islink(self._join(fname))
430 430 except OSError, e:
431 431 if e.errno != errno.ENOENT:
432 432 raise
433 433 else:
434 434 islink, isexec = mode
435 435 if islink:
436 436 self.opener.symlink(''.join(lines), fname)
437 437 else:
438 438 self.opener(fname, 'w').writelines(lines)
439 439 if isexec:
440 440 util.setflags(self._join(fname), False, True)
441 441
442 442 def unlink(self, fname):
443 443 try:
444 444 util.unlinkpath(self._join(fname))
445 445 except OSError, inst:
446 446 if inst.errno != errno.ENOENT:
447 447 raise
448 448
449 449 def writerej(self, fname, failed, total, lines):
450 450 fname = fname + ".rej"
451 451 self.ui.warn(
452 452 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
453 453 (failed, total, fname))
454 454 fp = self.opener(fname, 'w')
455 455 fp.writelines(lines)
456 456 fp.close()
457 457
458 458 def copy(self, src, dst):
459 459 basedir = self.opener.base
460 460 abssrc, absdst = [scmutil.canonpath(basedir, basedir, x)
461 461 for x in [src, dst]]
462 462 if os.path.lexists(absdst):
463 463 raise util.Abort(_("cannot create %s: destination already exists")
464 464 % dst)
465 465 dstdir = os.path.dirname(absdst)
466 466 if dstdir and not os.path.isdir(dstdir):
467 467 try:
468 468 os.makedirs(dstdir)
469 469 except IOError:
470 470 raise util.Abort(
471 471 _("cannot create %s: unable to create destination directory")
472 472 % dst)
473 473 util.copyfile(abssrc, absdst)
474 474
475 475 def exists(self, fname):
476 476 return os.path.lexists(self._join(fname))
477 477
478 478 def setmode(self, fname, islink, isexec):
479 479 util.setflags(self._join(fname), islink, isexec)
480 480
481 class workingbackend(fsbackend):
482 def __init__(self, ui, repo, similarity):
483 super(workingbackend, self).__init__(ui, repo.root)
484 self.repo = repo
485 self.similarity = similarity
486 self.removed = set()
487 self.changed = set()
488 self.copied = []
489
490 def writelines(self, fname, lines, mode):
491 super(workingbackend, self).writelines(fname, lines, mode)
492 self.changed.add(fname)
493
494 def unlink(self, fname):
495 super(workingbackend, self).unlink(fname)
496 self.removed.add(fname)
497 self.changed.add(fname)
498
499 def copy(self, src, dst):
500 super(workingbackend, self).copy(src, dst)
501 self.copied.append((src, dst))
502 self.changed.add(dst)
503
504 def setmode(self, fname, islink, isexec):
505 super(workingbackend, self).setmode(fname, islink, isexec)
506 self.changed.add(fname)
507
508 def close(self):
509 wctx = self.repo[None]
510 addremoved = set(self.changed)
511 for src, dst in self.copied:
512 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
513 addremoved.discard(src)
514 if (not self.similarity) and self.removed:
515 wctx.remove(sorted(self.removed))
516 if addremoved:
517 cwd = self.repo.getcwd()
518 if cwd:
519 addremoved = [util.pathto(self.repo.root, cwd, f)
520 for f in addremoved]
521 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
522 return sorted(self.changed)
523
481 524 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
482 525 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
483 526 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
484 527 eolmodes = ['strict', 'crlf', 'lf', 'auto']
485 528
486 529 class patchfile(object):
487 530 def __init__(self, ui, fname, backend, mode, missing=False,
488 531 eolmode='strict'):
489 532 self.fname = fname
490 533 self.eolmode = eolmode
491 534 self.eol = None
492 535 self.backend = backend
493 536 self.ui = ui
494 537 self.lines = []
495 538 self.exists = False
496 539 self.missing = missing
497 540 self.mode = mode
498 541 if not missing:
499 542 try:
500 543 self.lines = self.backend.readlines(fname)
501 544 if self.lines:
502 545 # Normalize line endings
503 546 if self.lines[0].endswith('\r\n'):
504 547 self.eol = '\r\n'
505 548 elif self.lines[0].endswith('\n'):
506 549 self.eol = '\n'
507 550 if eolmode != 'strict':
508 551 nlines = []
509 552 for l in self.lines:
510 553 if l.endswith('\r\n'):
511 554 l = l[:-2] + '\n'
512 555 nlines.append(l)
513 556 self.lines = nlines
514 557 self.exists = True
515 558 except IOError:
516 559 pass
517 560 else:
518 561 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
519 562
520 563 self.hash = {}
521 564 self.dirty = 0
522 565 self.offset = 0
523 566 self.skew = 0
524 567 self.rej = []
525 568 self.fileprinted = False
526 569 self.printfile(False)
527 570 self.hunks = 0
528 571
529 572 def writelines(self, fname, lines, mode):
530 573 if self.eolmode == 'auto':
531 574 eol = self.eol
532 575 elif self.eolmode == 'crlf':
533 576 eol = '\r\n'
534 577 else:
535 578 eol = '\n'
536 579
537 580 if self.eolmode != 'strict' and eol and eol != '\n':
538 581 rawlines = []
539 582 for l in lines:
540 583 if l and l[-1] == '\n':
541 584 l = l[:-1] + eol
542 585 rawlines.append(l)
543 586 lines = rawlines
544 587
545 588 self.backend.writelines(fname, lines, mode)
546 589
547 590 def printfile(self, warn):
548 591 if self.fileprinted:
549 592 return
550 593 if warn or self.ui.verbose:
551 594 self.fileprinted = True
552 595 s = _("patching file %s\n") % self.fname
553 596 if warn:
554 597 self.ui.warn(s)
555 598 else:
556 599 self.ui.note(s)
557 600
558 601
559 602 def findlines(self, l, linenum):
560 603 # looks through the hash and finds candidate lines. The
561 604 # result is a list of line numbers sorted based on distance
562 605 # from linenum
563 606
564 607 cand = self.hash.get(l, [])
565 608 if len(cand) > 1:
566 609 # resort our list of potentials forward then back.
567 610 cand.sort(key=lambda x: abs(x - linenum))
568 611 return cand
569 612
570 613 def write_rej(self):
571 614 # our rejects are a little different from patch(1). This always
572 615 # creates rejects in the same form as the original patch. A file
573 616 # header is inserted so that you can run the reject through patch again
574 617 # without having to type the filename.
575 618 if not self.rej:
576 619 return
577 620 base = os.path.basename(self.fname)
578 621 lines = ["--- %s\n+++ %s\n" % (base, base)]
579 622 for x in self.rej:
580 623 for l in x.hunk:
581 624 lines.append(l)
582 625 if l[-1] != '\n':
583 626 lines.append("\n\ No newline at end of file\n")
584 627 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
585 628
586 629 def apply(self, h):
587 630 if not h.complete():
588 631 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
589 632 (h.number, h.desc, len(h.a), h.lena, len(h.b),
590 633 h.lenb))
591 634
592 635 self.hunks += 1
593 636
594 637 if self.missing:
595 638 self.rej.append(h)
596 639 return -1
597 640
598 641 if self.exists and h.createfile():
599 642 self.ui.warn(_("file %s already exists\n") % self.fname)
600 643 self.rej.append(h)
601 644 return -1
602 645
603 646 if isinstance(h, binhunk):
604 647 if h.rmfile():
605 648 self.backend.unlink(self.fname)
606 649 else:
607 650 self.lines[:] = h.new()
608 651 self.offset += len(h.new())
609 652 self.dirty = True
610 653 return 0
611 654
612 655 horig = h
613 656 if (self.eolmode in ('crlf', 'lf')
614 657 or self.eolmode == 'auto' and self.eol):
615 658 # If new eols are going to be normalized, then normalize
616 659 # hunk data before patching. Otherwise, preserve input
617 660 # line-endings.
618 661 h = h.getnormalized()
619 662
620 663 # fast case first, no offsets, no fuzz
621 664 old = h.old()
622 665 # patch starts counting at 1 unless we are adding the file
623 666 if h.starta == 0:
624 667 start = 0
625 668 else:
626 669 start = h.starta + self.offset - 1
627 670 orig_start = start
628 671 # if there's skew we want to emit the "(offset %d lines)" even
629 672 # when the hunk cleanly applies at start + skew, so skip the
630 673 # fast case code
631 674 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
632 675 if h.rmfile():
633 676 self.backend.unlink(self.fname)
634 677 else:
635 678 self.lines[start : start + h.lena] = h.new()
636 679 self.offset += h.lenb - h.lena
637 680 self.dirty = True
638 681 return 0
639 682
640 683 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
641 684 self.hash = {}
642 685 for x, s in enumerate(self.lines):
643 686 self.hash.setdefault(s, []).append(x)
644 687 if h.hunk[-1][0] != ' ':
645 688 # if the hunk tried to put something at the bottom of the file
646 689 # override the start line and use eof here
647 690 search_start = len(self.lines)
648 691 else:
649 692 search_start = orig_start + self.skew
650 693
651 694 for fuzzlen in xrange(3):
652 695 for toponly in [True, False]:
653 696 old = h.old(fuzzlen, toponly)
654 697
655 698 cand = self.findlines(old[0][1:], search_start)
656 699 for l in cand:
657 700 if diffhelpers.testhunk(old, self.lines, l) == 0:
658 701 newlines = h.new(fuzzlen, toponly)
659 702 self.lines[l : l + len(old)] = newlines
660 703 self.offset += len(newlines) - len(old)
661 704 self.skew = l - orig_start
662 705 self.dirty = True
663 706 offset = l - orig_start - fuzzlen
664 707 if fuzzlen:
665 708 msg = _("Hunk #%d succeeded at %d "
666 709 "with fuzz %d "
667 710 "(offset %d lines).\n")
668 711 self.printfile(True)
669 712 self.ui.warn(msg %
670 713 (h.number, l + 1, fuzzlen, offset))
671 714 else:
672 715 msg = _("Hunk #%d succeeded at %d "
673 716 "(offset %d lines).\n")
674 717 self.ui.note(msg % (h.number, l + 1, offset))
675 718 return fuzzlen
676 719 self.printfile(True)
677 720 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
678 721 self.rej.append(horig)
679 722 return -1
680 723
681 724 def close(self):
682 725 if self.dirty:
683 726 self.writelines(self.fname, self.lines, self.mode)
684 727 self.write_rej()
685 728 return len(self.rej)
686 729
687 730 class hunk(object):
688 731 def __init__(self, desc, num, lr, context, create=False, remove=False):
689 732 self.number = num
690 733 self.desc = desc
691 734 self.hunk = [desc]
692 735 self.a = []
693 736 self.b = []
694 737 self.starta = self.lena = None
695 738 self.startb = self.lenb = None
696 739 if lr is not None:
697 740 if context:
698 741 self.read_context_hunk(lr)
699 742 else:
700 743 self.read_unified_hunk(lr)
701 744 self.create = create
702 745 self.remove = remove and not create
703 746
704 747 def getnormalized(self):
705 748 """Return a copy with line endings normalized to LF."""
706 749
707 750 def normalize(lines):
708 751 nlines = []
709 752 for line in lines:
710 753 if line.endswith('\r\n'):
711 754 line = line[:-2] + '\n'
712 755 nlines.append(line)
713 756 return nlines
714 757
715 758 # Dummy object, it is rebuilt manually
716 759 nh = hunk(self.desc, self.number, None, None, False, False)
717 760 nh.number = self.number
718 761 nh.desc = self.desc
719 762 nh.hunk = self.hunk
720 763 nh.a = normalize(self.a)
721 764 nh.b = normalize(self.b)
722 765 nh.starta = self.starta
723 766 nh.startb = self.startb
724 767 nh.lena = self.lena
725 768 nh.lenb = self.lenb
726 769 nh.create = self.create
727 770 nh.remove = self.remove
728 771 return nh
729 772
730 773 def read_unified_hunk(self, lr):
731 774 m = unidesc.match(self.desc)
732 775 if not m:
733 776 raise PatchError(_("bad hunk #%d") % self.number)
734 777 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
735 778 if self.lena is None:
736 779 self.lena = 1
737 780 else:
738 781 self.lena = int(self.lena)
739 782 if self.lenb is None:
740 783 self.lenb = 1
741 784 else:
742 785 self.lenb = int(self.lenb)
743 786 self.starta = int(self.starta)
744 787 self.startb = int(self.startb)
745 788 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
746 789 # if we hit eof before finishing out the hunk, the last line will
747 790 # be zero length. Lets try to fix it up.
748 791 while len(self.hunk[-1]) == 0:
749 792 del self.hunk[-1]
750 793 del self.a[-1]
751 794 del self.b[-1]
752 795 self.lena -= 1
753 796 self.lenb -= 1
754 797 self._fixnewline(lr)
755 798
756 799 def read_context_hunk(self, lr):
757 800 self.desc = lr.readline()
758 801 m = contextdesc.match(self.desc)
759 802 if not m:
760 803 raise PatchError(_("bad hunk #%d") % self.number)
761 804 foo, self.starta, foo2, aend, foo3 = m.groups()
762 805 self.starta = int(self.starta)
763 806 if aend is None:
764 807 aend = self.starta
765 808 self.lena = int(aend) - self.starta
766 809 if self.starta:
767 810 self.lena += 1
768 811 for x in xrange(self.lena):
769 812 l = lr.readline()
770 813 if l.startswith('---'):
771 814 # lines addition, old block is empty
772 815 lr.push(l)
773 816 break
774 817 s = l[2:]
775 818 if l.startswith('- ') or l.startswith('! '):
776 819 u = '-' + s
777 820 elif l.startswith(' '):
778 821 u = ' ' + s
779 822 else:
780 823 raise PatchError(_("bad hunk #%d old text line %d") %
781 824 (self.number, x))
782 825 self.a.append(u)
783 826 self.hunk.append(u)
784 827
785 828 l = lr.readline()
786 829 if l.startswith('\ '):
787 830 s = self.a[-1][:-1]
788 831 self.a[-1] = s
789 832 self.hunk[-1] = s
790 833 l = lr.readline()
791 834 m = contextdesc.match(l)
792 835 if not m:
793 836 raise PatchError(_("bad hunk #%d") % self.number)
794 837 foo, self.startb, foo2, bend, foo3 = m.groups()
795 838 self.startb = int(self.startb)
796 839 if bend is None:
797 840 bend = self.startb
798 841 self.lenb = int(bend) - self.startb
799 842 if self.startb:
800 843 self.lenb += 1
801 844 hunki = 1
802 845 for x in xrange(self.lenb):
803 846 l = lr.readline()
804 847 if l.startswith('\ '):
805 848 # XXX: the only way to hit this is with an invalid line range.
806 849 # The no-eol marker is not counted in the line range, but I
807 850 # guess there are diff(1) out there which behave differently.
808 851 s = self.b[-1][:-1]
809 852 self.b[-1] = s
810 853 self.hunk[hunki - 1] = s
811 854 continue
812 855 if not l:
813 856 # line deletions, new block is empty and we hit EOF
814 857 lr.push(l)
815 858 break
816 859 s = l[2:]
817 860 if l.startswith('+ ') or l.startswith('! '):
818 861 u = '+' + s
819 862 elif l.startswith(' '):
820 863 u = ' ' + s
821 864 elif len(self.b) == 0:
822 865 # line deletions, new block is empty
823 866 lr.push(l)
824 867 break
825 868 else:
826 869 raise PatchError(_("bad hunk #%d old text line %d") %
827 870 (self.number, x))
828 871 self.b.append(s)
829 872 while True:
830 873 if hunki >= len(self.hunk):
831 874 h = ""
832 875 else:
833 876 h = self.hunk[hunki]
834 877 hunki += 1
835 878 if h == u:
836 879 break
837 880 elif h.startswith('-'):
838 881 continue
839 882 else:
840 883 self.hunk.insert(hunki - 1, u)
841 884 break
842 885
843 886 if not self.a:
844 887 # this happens when lines were only added to the hunk
845 888 for x in self.hunk:
846 889 if x.startswith('-') or x.startswith(' '):
847 890 self.a.append(x)
848 891 if not self.b:
849 892 # this happens when lines were only deleted from the hunk
850 893 for x in self.hunk:
851 894 if x.startswith('+') or x.startswith(' '):
852 895 self.b.append(x[1:])
853 896 # @@ -start,len +start,len @@
854 897 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
855 898 self.startb, self.lenb)
856 899 self.hunk[0] = self.desc
857 900 self._fixnewline(lr)
858 901
859 902 def _fixnewline(self, lr):
860 903 l = lr.readline()
861 904 if l.startswith('\ '):
862 905 diffhelpers.fix_newline(self.hunk, self.a, self.b)
863 906 else:
864 907 lr.push(l)
865 908
866 909 def complete(self):
867 910 return len(self.a) == self.lena and len(self.b) == self.lenb
868 911
869 912 def createfile(self):
870 913 return self.starta == 0 and self.lena == 0 and self.create
871 914
872 915 def rmfile(self):
873 916 return self.startb == 0 and self.lenb == 0 and self.remove
874 917
875 918 def fuzzit(self, l, fuzz, toponly):
876 919 # this removes context lines from the top and bottom of list 'l'. It
877 920 # checks the hunk to make sure only context lines are removed, and then
878 921 # returns a new shortened list of lines.
879 922 fuzz = min(fuzz, len(l)-1)
880 923 if fuzz:
881 924 top = 0
882 925 bot = 0
883 926 hlen = len(self.hunk)
884 927 for x in xrange(hlen - 1):
885 928 # the hunk starts with the @@ line, so use x+1
886 929 if self.hunk[x + 1][0] == ' ':
887 930 top += 1
888 931 else:
889 932 break
890 933 if not toponly:
891 934 for x in xrange(hlen - 1):
892 935 if self.hunk[hlen - bot - 1][0] == ' ':
893 936 bot += 1
894 937 else:
895 938 break
896 939
897 940 # top and bot now count context in the hunk
898 941 # adjust them if either one is short
899 942 context = max(top, bot, 3)
900 943 if bot < context:
901 944 bot = max(0, fuzz - (context - bot))
902 945 else:
903 946 bot = min(fuzz, bot)
904 947 if top < context:
905 948 top = max(0, fuzz - (context - top))
906 949 else:
907 950 top = min(fuzz, top)
908 951
909 952 return l[top:len(l)-bot]
910 953 return l
911 954
912 955 def old(self, fuzz=0, toponly=False):
913 956 return self.fuzzit(self.a, fuzz, toponly)
914 957
915 958 def new(self, fuzz=0, toponly=False):
916 959 return self.fuzzit(self.b, fuzz, toponly)
917 960
918 961 class binhunk:
919 962 'A binary patch file. Only understands literals so far.'
920 963 def __init__(self, gitpatch):
921 964 self.gitpatch = gitpatch
922 965 self.text = None
923 966 self.hunk = ['GIT binary patch\n']
924 967
925 968 def createfile(self):
926 969 return self.gitpatch.op == 'ADD'
927 970
928 971 def rmfile(self):
929 972 return self.gitpatch.op == 'DELETE'
930 973
931 974 def complete(self):
932 975 return self.text is not None
933 976
934 977 def new(self):
935 978 return [self.text]
936 979
937 980 def extract(self, lr):
938 981 line = lr.readline()
939 982 self.hunk.append(line)
940 983 while line and not line.startswith('literal '):
941 984 line = lr.readline()
942 985 self.hunk.append(line)
943 986 if not line:
944 987 raise PatchError(_('could not extract binary patch'))
945 988 size = int(line[8:].rstrip())
946 989 dec = []
947 990 line = lr.readline()
948 991 self.hunk.append(line)
949 992 while len(line) > 1:
950 993 l = line[0]
951 994 if l <= 'Z' and l >= 'A':
952 995 l = ord(l) - ord('A') + 1
953 996 else:
954 997 l = ord(l) - ord('a') + 27
955 998 dec.append(base85.b85decode(line[1:-1])[:l])
956 999 line = lr.readline()
957 1000 self.hunk.append(line)
958 1001 text = zlib.decompress(''.join(dec))
959 1002 if len(text) != size:
960 1003 raise PatchError(_('binary patch is %d bytes, not %d') %
961 1004 len(text), size)
962 1005 self.text = text
963 1006
964 1007 def parsefilename(str):
965 1008 # --- filename \t|space stuff
966 1009 s = str[4:].rstrip('\r\n')
967 1010 i = s.find('\t')
968 1011 if i < 0:
969 1012 i = s.find(' ')
970 1013 if i < 0:
971 1014 return s
972 1015 return s[:i]
973 1016
974 1017 def pathstrip(path, strip):
975 1018 pathlen = len(path)
976 1019 i = 0
977 1020 if strip == 0:
978 1021 return '', path.rstrip()
979 1022 count = strip
980 1023 while count > 0:
981 1024 i = path.find('/', i)
982 1025 if i == -1:
983 1026 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
984 1027 (count, strip, path))
985 1028 i += 1
986 1029 # consume '//' in the path
987 1030 while i < pathlen - 1 and path[i] == '/':
988 1031 i += 1
989 1032 count -= 1
990 1033 return path[:i].lstrip(), path[i:].rstrip()
991 1034
992 1035 def selectfile(backend, afile_orig, bfile_orig, hunk, strip):
993 1036 nulla = afile_orig == "/dev/null"
994 1037 nullb = bfile_orig == "/dev/null"
995 1038 abase, afile = pathstrip(afile_orig, strip)
996 1039 gooda = not nulla and backend.exists(afile)
997 1040 bbase, bfile = pathstrip(bfile_orig, strip)
998 1041 if afile == bfile:
999 1042 goodb = gooda
1000 1043 else:
1001 1044 goodb = not nullb and backend.exists(bfile)
1002 1045 createfunc = hunk.createfile
1003 1046 missing = not goodb and not gooda and not createfunc()
1004 1047
1005 1048 # some diff programs apparently produce patches where the afile is
1006 1049 # not /dev/null, but afile starts with bfile
1007 1050 abasedir = afile[:afile.rfind('/') + 1]
1008 1051 bbasedir = bfile[:bfile.rfind('/') + 1]
1009 1052 if missing and abasedir == bbasedir and afile.startswith(bfile):
1010 1053 # this isn't very pretty
1011 1054 hunk.create = True
1012 1055 if createfunc():
1013 1056 missing = False
1014 1057 else:
1015 1058 hunk.create = False
1016 1059
1017 1060 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1018 1061 # diff is between a file and its backup. In this case, the original
1019 1062 # file should be patched (see original mpatch code).
1020 1063 isbackup = (abase == bbase and bfile.startswith(afile))
1021 1064 fname = None
1022 1065 if not missing:
1023 1066 if gooda and goodb:
1024 1067 fname = isbackup and afile or bfile
1025 1068 elif gooda:
1026 1069 fname = afile
1027 1070
1028 1071 if not fname:
1029 1072 if not nullb:
1030 1073 fname = isbackup and afile or bfile
1031 1074 elif not nulla:
1032 1075 fname = afile
1033 1076 else:
1034 1077 raise PatchError(_("undefined source and destination files"))
1035 1078
1036 1079 return fname, missing
1037 1080
1038 1081 def scangitpatch(lr, firstline):
1039 1082 """
1040 1083 Git patches can emit:
1041 1084 - rename a to b
1042 1085 - change b
1043 1086 - copy a to c
1044 1087 - change c
1045 1088
1046 1089 We cannot apply this sequence as-is, the renamed 'a' could not be
1047 1090 found for it would have been renamed already. And we cannot copy
1048 1091 from 'b' instead because 'b' would have been changed already. So
1049 1092 we scan the git patch for copy and rename commands so we can
1050 1093 perform the copies ahead of time.
1051 1094 """
1052 1095 pos = 0
1053 1096 try:
1054 1097 pos = lr.fp.tell()
1055 1098 fp = lr.fp
1056 1099 except IOError:
1057 1100 fp = cStringIO.StringIO(lr.fp.read())
1058 1101 gitlr = linereader(fp, lr.textmode)
1059 1102 gitlr.push(firstline)
1060 1103 gitpatches = readgitpatch(gitlr)
1061 1104 fp.seek(pos)
1062 1105 return gitpatches
1063 1106
1064 1107 def iterhunks(fp):
1065 1108 """Read a patch and yield the following events:
1066 1109 - ("file", afile, bfile, firsthunk): select a new target file.
1067 1110 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1068 1111 "file" event.
1069 1112 - ("git", gitchanges): current diff is in git format, gitchanges
1070 1113 maps filenames to gitpatch records. Unique event.
1071 1114 """
1072 1115 changed = {}
1073 1116 afile = ""
1074 1117 bfile = ""
1075 1118 state = None
1076 1119 hunknum = 0
1077 1120 emitfile = newfile = False
1078 1121 git = False
1079 1122
1080 1123 # our states
1081 1124 BFILE = 1
1082 1125 context = None
1083 1126 lr = linereader(fp)
1084 1127
1085 1128 while True:
1086 1129 x = lr.readline()
1087 1130 if not x:
1088 1131 break
1089 1132 if (state == BFILE and ((not context and x[0] == '@') or
1090 1133 ((context is not False) and x.startswith('***************')))):
1091 1134 if context is None and x.startswith('***************'):
1092 1135 context = True
1093 1136 gpatch = changed.get(bfile)
1094 1137 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1095 1138 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1096 1139 h = hunk(x, hunknum + 1, lr, context, create, remove)
1097 1140 hunknum += 1
1098 1141 if emitfile:
1099 1142 emitfile = False
1100 1143 yield 'file', (afile, bfile, h, gpatch and gpatch.mode or None)
1101 1144 yield 'hunk', h
1102 1145 elif state == BFILE and x.startswith('GIT binary patch'):
1103 1146 gpatch = changed[bfile]
1104 1147 h = binhunk(gpatch)
1105 1148 hunknum += 1
1106 1149 if emitfile:
1107 1150 emitfile = False
1108 1151 yield 'file', ('a/' + afile, 'b/' + bfile, h,
1109 1152 gpatch and gpatch.mode or None)
1110 1153 h.extract(lr)
1111 1154 yield 'hunk', h
1112 1155 elif x.startswith('diff --git'):
1113 1156 # check for git diff, scanning the whole patch file if needed
1114 1157 m = gitre.match(x)
1115 1158 if m:
1116 1159 afile, bfile = m.group(1, 2)
1117 1160 if not git:
1118 1161 git = True
1119 1162 gitpatches = scangitpatch(lr, x)
1120 1163 yield 'git', gitpatches
1121 1164 for gp in gitpatches:
1122 1165 changed[gp.path] = gp
1123 1166 # else error?
1124 1167 # copy/rename + modify should modify target, not source
1125 1168 gp = changed.get(bfile)
1126 1169 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1127 1170 or gp.mode):
1128 1171 afile = bfile
1129 1172 newfile = True
1130 1173 elif x.startswith('---'):
1131 1174 # check for a unified diff
1132 1175 l2 = lr.readline()
1133 1176 if not l2.startswith('+++'):
1134 1177 lr.push(l2)
1135 1178 continue
1136 1179 newfile = True
1137 1180 context = False
1138 1181 afile = parsefilename(x)
1139 1182 bfile = parsefilename(l2)
1140 1183 elif x.startswith('***'):
1141 1184 # check for a context diff
1142 1185 l2 = lr.readline()
1143 1186 if not l2.startswith('---'):
1144 1187 lr.push(l2)
1145 1188 continue
1146 1189 l3 = lr.readline()
1147 1190 lr.push(l3)
1148 1191 if not l3.startswith("***************"):
1149 1192 lr.push(l2)
1150 1193 continue
1151 1194 newfile = True
1152 1195 context = True
1153 1196 afile = parsefilename(x)
1154 1197 bfile = parsefilename(l2)
1155 1198
1156 1199 if newfile:
1157 1200 newfile = False
1158 1201 emitfile = True
1159 1202 state = BFILE
1160 1203 hunknum = 0
1161 1204
1162 1205 def applydiff(ui, fp, changed, backend, strip=1, eolmode='strict'):
1163 1206 """Reads a patch from fp and tries to apply it.
1164 1207
1165 1208 The dict 'changed' is filled in with all of the filenames changed
1166 1209 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1167 1210 found and 1 if there was any fuzz.
1168 1211
1169 1212 If 'eolmode' is 'strict', the patch content and patched file are
1170 1213 read in binary mode. Otherwise, line endings are ignored when
1171 1214 patching then normalized according to 'eolmode'.
1172
1173 Callers probably want to call '_updatedir' after this to
1174 apply certain categories of changes not done by this function.
1175 1215 """
1176 1216 return _applydiff(ui, fp, patchfile, backend, changed, strip=strip,
1177 1217 eolmode=eolmode)
1178 1218
1179 1219 def _applydiff(ui, fp, patcher, backend, changed, strip=1, eolmode='strict'):
1180 1220 rejects = 0
1181 1221 err = 0
1182 1222 current_file = None
1183 1223
1184 1224 for state, values in iterhunks(fp):
1185 1225 if state == 'hunk':
1186 1226 if not current_file:
1187 1227 continue
1188 1228 ret = current_file.apply(values)
1189 1229 if ret >= 0:
1190 1230 changed.setdefault(current_file.fname, None)
1191 1231 if ret > 0:
1192 1232 err = 1
1193 1233 elif state == 'file':
1194 1234 if current_file:
1195 1235 rejects += current_file.close()
1196 1236 afile, bfile, first_hunk, mode = values
1197 1237 try:
1198 1238 current_file, missing = selectfile(backend, afile, bfile,
1199 1239 first_hunk, strip)
1200 1240 current_file = patcher(ui, current_file, backend, mode,
1201 1241 missing=missing, eolmode=eolmode)
1202 1242 except PatchError, inst:
1203 1243 ui.warn(str(inst) + '\n')
1204 1244 current_file = None
1205 1245 rejects += 1
1206 1246 continue
1207 1247 elif state == 'git':
1208 1248 for gp in values:
1209 1249 gp.path = pathstrip(gp.path, strip - 1)[1]
1210 1250 if gp.oldpath:
1211 1251 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1212 1252 if gp.op in ('COPY', 'RENAME'):
1213 1253 backend.copy(gp.oldpath, gp.path)
1214 1254 changed[gp.path] = gp
1215 1255 else:
1216 1256 raise util.Abort(_('unsupported parser state: %s') % state)
1217 1257
1218 1258 if current_file:
1219 1259 rejects += current_file.close()
1220 1260
1221 1261 # Handle mode changes without hunk
1222 1262 removed = set()
1223 1263 for gp in changed.itervalues():
1224 1264 if not gp:
1225 1265 continue
1226 1266 if gp.op == 'DELETE':
1227 1267 removed.add(gp.path)
1228 1268 continue
1229 1269 if gp.op == 'RENAME':
1230 1270 removed.add(gp.oldpath)
1231 1271 if gp.mode:
1232 1272 if gp.op == 'ADD' and not backend.exists(gp.path):
1233 1273 # Added files without content have no hunk and must be created
1234 1274 backend.writelines(gp.path, [], gp.mode)
1235 1275 else:
1236 1276 backend.setmode(gp.path, gp.mode[0], gp.mode[1])
1237 1277 for path in sorted(removed):
1238 1278 backend.unlink(path)
1239 1279
1240 1280 if rejects:
1241 1281 return -1
1242 1282 return err
1243 1283
1244 1284 def _updatedir(ui, repo, patches, similarity=0):
1245 1285 '''Update dirstate after patch application according to metadata'''
1246 1286 if not patches:
1247 1287 return []
1248 1288 copies = []
1249 1289 removes = set()
1250 1290 cfiles = patches.keys()
1251 1291 cwd = repo.getcwd()
1252 1292 if cwd:
1253 1293 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1254 1294 for f in patches:
1255 1295 gp = patches[f]
1256 1296 if not gp:
1257 1297 continue
1258 1298 if gp.op == 'RENAME':
1259 1299 copies.append((gp.oldpath, gp.path))
1260 1300 removes.add(gp.oldpath)
1261 1301 elif gp.op == 'COPY':
1262 1302 copies.append((gp.oldpath, gp.path))
1263 1303 elif gp.op == 'DELETE':
1264 1304 removes.add(gp.path)
1265 1305
1266 1306 wctx = repo[None]
1267 1307 for src, dst in copies:
1268 1308 scmutil.dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
1269 1309 if (not similarity) and removes:
1270 1310 wctx.remove(sorted(removes))
1271 1311
1272 1312 scmutil.addremove(repo, cfiles, similarity=similarity)
1273 1313 files = patches.keys()
1274 1314 files.extend([r for r in removes if r not in files])
1275 1315 return sorted(files)
1276 1316
1277 1317 def _externalpatch(patcher, patchname, ui, strip, cwd, files):
1278 1318 """use <patcher> to apply <patchname> to the working directory.
1279 1319 returns whether patch was applied with fuzz factor."""
1280 1320
1281 1321 fuzz = False
1282 1322 args = []
1283 1323 if cwd:
1284 1324 args.append('-d %s' % util.shellquote(cwd))
1285 1325 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1286 1326 util.shellquote(patchname)))
1287 1327
1288 1328 for line in fp:
1289 1329 line = line.rstrip()
1290 1330 ui.note(line + '\n')
1291 1331 if line.startswith('patching file '):
1292 1332 pf = util.parsepatchoutput(line)
1293 1333 printed_file = False
1294 1334 files.setdefault(pf, None)
1295 1335 elif line.find('with fuzz') >= 0:
1296 1336 fuzz = True
1297 1337 if not printed_file:
1298 1338 ui.warn(pf + '\n')
1299 1339 printed_file = True
1300 1340 ui.warn(line + '\n')
1301 1341 elif line.find('saving rejects to file') >= 0:
1302 1342 ui.warn(line + '\n')
1303 1343 elif line.find('FAILED') >= 0:
1304 1344 if not printed_file:
1305 1345 ui.warn(pf + '\n')
1306 1346 printed_file = True
1307 1347 ui.warn(line + '\n')
1308 1348 code = fp.close()
1309 1349 if code:
1310 1350 raise PatchError(_("patch command failed: %s") %
1311 1351 util.explainexit(code)[0])
1312 1352 return fuzz
1313 1353
1314 def internalpatch(ui, repo, patchobj, strip, cwd, files=None, eolmode='strict',
1354 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1315 1355 similarity=0):
1316 1356 """use builtin patch to apply <patchobj> to the working directory.
1317 1357 returns whether patch was applied with fuzz factor."""
1318 1358
1319 1359 if files is None:
1320 1360 files = {}
1321 1361 if eolmode is None:
1322 1362 eolmode = ui.config('patch', 'eol', 'strict')
1323 1363 if eolmode.lower() not in eolmodes:
1324 1364 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1325 1365 eolmode = eolmode.lower()
1326 1366
1327 backend = fsbackend(ui, cwd)
1367 backend = workingbackend(ui, repo, similarity)
1328 1368 try:
1329 1369 fp = open(patchobj, 'rb')
1330 1370 except TypeError:
1331 1371 fp = patchobj
1332 1372 try:
1333 1373 ret = applydiff(ui, fp, files, backend, strip=strip, eolmode=eolmode)
1334 1374 finally:
1335 1375 if fp != patchobj:
1336 1376 fp.close()
1337 touched = _updatedir(ui, repo, files, similarity)
1338 files.update(dict.fromkeys(touched))
1377 files.update(dict.fromkeys(backend.close()))
1339 1378 if ret < 0:
1340 1379 raise PatchError(_('patch failed to apply'))
1341 1380 return ret > 0
1342 1381
1343 1382 def patch(ui, repo, patchname, strip=1, cwd=None, files=None, eolmode='strict',
1344 1383 similarity=0):
1345 1384 """Apply <patchname> to the working directory.
1346 1385
1347 1386 'eolmode' specifies how end of lines should be handled. It can be:
1348 1387 - 'strict': inputs are read in binary mode, EOLs are preserved
1349 1388 - 'crlf': EOLs are ignored when patching and reset to CRLF
1350 1389 - 'lf': EOLs are ignored when patching and reset to LF
1351 1390 - None: get it from user settings, default to 'strict'
1352 1391 'eolmode' is ignored when using an external patcher program.
1353 1392
1354 1393 Returns whether patch was applied with fuzz factor.
1355 1394 """
1356 1395 patcher = ui.config('ui', 'patch')
1357 1396 if files is None:
1358 1397 files = {}
1359 1398 try:
1360 1399 if patcher:
1361 1400 try:
1362 1401 return _externalpatch(patcher, patchname, ui, strip, cwd,
1363 1402 files)
1364 1403 finally:
1365 1404 touched = _updatedir(ui, repo, files, similarity)
1366 1405 files.update(dict.fromkeys(touched))
1367 return internalpatch(ui, repo, patchname, strip, cwd, files, eolmode,
1406 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1368 1407 similarity)
1369 1408 except PatchError, err:
1370 1409 raise util.Abort(str(err))
1371 1410
1372 1411 def changedfiles(ui, repo, patchpath, strip=1):
1373 1412 backend = fsbackend(ui, repo.root)
1374 1413 fp = open(patchpath, 'rb')
1375 1414 try:
1376 1415 changed = set()
1377 1416 for state, values in iterhunks(fp):
1378 1417 if state == 'hunk':
1379 1418 continue
1380 1419 elif state == 'file':
1381 1420 afile, bfile, first_hunk, mode = values
1382 1421 current_file, missing = selectfile(backend, afile, bfile,
1383 1422 first_hunk, strip)
1384 1423 changed.add(current_file)
1385 1424 elif state == 'git':
1386 1425 for gp in values:
1387 1426 gp.path = pathstrip(gp.path, strip - 1)[1]
1388 1427 changed.add(gp.path)
1389 1428 if gp.oldpath:
1390 1429 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1391 1430 if gp.op == 'RENAME':
1392 1431 changed.add(gp.oldpath)
1393 1432 else:
1394 1433 raise util.Abort(_('unsupported parser state: %s') % state)
1395 1434 return changed
1396 1435 finally:
1397 1436 fp.close()
1398 1437
1399 1438 def b85diff(to, tn):
1400 1439 '''print base85-encoded binary diff'''
1401 1440 def gitindex(text):
1402 1441 if not text:
1403 1442 return hex(nullid)
1404 1443 l = len(text)
1405 1444 s = util.sha1('blob %d\0' % l)
1406 1445 s.update(text)
1407 1446 return s.hexdigest()
1408 1447
1409 1448 def fmtline(line):
1410 1449 l = len(line)
1411 1450 if l <= 26:
1412 1451 l = chr(ord('A') + l - 1)
1413 1452 else:
1414 1453 l = chr(l - 26 + ord('a') - 1)
1415 1454 return '%c%s\n' % (l, base85.b85encode(line, True))
1416 1455
1417 1456 def chunk(text, csize=52):
1418 1457 l = len(text)
1419 1458 i = 0
1420 1459 while i < l:
1421 1460 yield text[i:i + csize]
1422 1461 i += csize
1423 1462
1424 1463 tohash = gitindex(to)
1425 1464 tnhash = gitindex(tn)
1426 1465 if tohash == tnhash:
1427 1466 return ""
1428 1467
1429 1468 # TODO: deltas
1430 1469 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1431 1470 (tohash, tnhash, len(tn))]
1432 1471 for l in chunk(zlib.compress(tn)):
1433 1472 ret.append(fmtline(l))
1434 1473 ret.append('\n')
1435 1474 return ''.join(ret)
1436 1475
1437 1476 class GitDiffRequired(Exception):
1438 1477 pass
1439 1478
1440 1479 def diffopts(ui, opts=None, untrusted=False):
1441 1480 def get(key, name=None, getter=ui.configbool):
1442 1481 return ((opts and opts.get(key)) or
1443 1482 getter('diff', name or key, None, untrusted=untrusted))
1444 1483 return mdiff.diffopts(
1445 1484 text=opts and opts.get('text'),
1446 1485 git=get('git'),
1447 1486 nodates=get('nodates'),
1448 1487 showfunc=get('show_function', 'showfunc'),
1449 1488 ignorews=get('ignore_all_space', 'ignorews'),
1450 1489 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1451 1490 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1452 1491 context=get('unified', getter=ui.config))
1453 1492
1454 1493 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1455 1494 losedatafn=None, prefix=''):
1456 1495 '''yields diff of changes to files between two nodes, or node and
1457 1496 working directory.
1458 1497
1459 1498 if node1 is None, use first dirstate parent instead.
1460 1499 if node2 is None, compare node1 with working directory.
1461 1500
1462 1501 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1463 1502 every time some change cannot be represented with the current
1464 1503 patch format. Return False to upgrade to git patch format, True to
1465 1504 accept the loss or raise an exception to abort the diff. It is
1466 1505 called with the name of current file being diffed as 'fn'. If set
1467 1506 to None, patches will always be upgraded to git format when
1468 1507 necessary.
1469 1508
1470 1509 prefix is a filename prefix that is prepended to all filenames on
1471 1510 display (used for subrepos).
1472 1511 '''
1473 1512
1474 1513 if opts is None:
1475 1514 opts = mdiff.defaultopts
1476 1515
1477 1516 if not node1 and not node2:
1478 1517 node1 = repo.dirstate.p1()
1479 1518
1480 1519 def lrugetfilectx():
1481 1520 cache = {}
1482 1521 order = []
1483 1522 def getfilectx(f, ctx):
1484 1523 fctx = ctx.filectx(f, filelog=cache.get(f))
1485 1524 if f not in cache:
1486 1525 if len(cache) > 20:
1487 1526 del cache[order.pop(0)]
1488 1527 cache[f] = fctx.filelog()
1489 1528 else:
1490 1529 order.remove(f)
1491 1530 order.append(f)
1492 1531 return fctx
1493 1532 return getfilectx
1494 1533 getfilectx = lrugetfilectx()
1495 1534
1496 1535 ctx1 = repo[node1]
1497 1536 ctx2 = repo[node2]
1498 1537
1499 1538 if not changes:
1500 1539 changes = repo.status(ctx1, ctx2, match=match)
1501 1540 modified, added, removed = changes[:3]
1502 1541
1503 1542 if not modified and not added and not removed:
1504 1543 return []
1505 1544
1506 1545 revs = None
1507 1546 if not repo.ui.quiet:
1508 1547 hexfunc = repo.ui.debugflag and hex or short
1509 1548 revs = [hexfunc(node) for node in [node1, node2] if node]
1510 1549
1511 1550 copy = {}
1512 1551 if opts.git or opts.upgrade:
1513 1552 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1514 1553
1515 1554 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1516 1555 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1517 1556 if opts.upgrade and not opts.git:
1518 1557 try:
1519 1558 def losedata(fn):
1520 1559 if not losedatafn or not losedatafn(fn=fn):
1521 1560 raise GitDiffRequired()
1522 1561 # Buffer the whole output until we are sure it can be generated
1523 1562 return list(difffn(opts.copy(git=False), losedata))
1524 1563 except GitDiffRequired:
1525 1564 return difffn(opts.copy(git=True), None)
1526 1565 else:
1527 1566 return difffn(opts, None)
1528 1567
1529 1568 def difflabel(func, *args, **kw):
1530 1569 '''yields 2-tuples of (output, label) based on the output of func()'''
1531 1570 prefixes = [('diff', 'diff.diffline'),
1532 1571 ('copy', 'diff.extended'),
1533 1572 ('rename', 'diff.extended'),
1534 1573 ('old', 'diff.extended'),
1535 1574 ('new', 'diff.extended'),
1536 1575 ('deleted', 'diff.extended'),
1537 1576 ('---', 'diff.file_a'),
1538 1577 ('+++', 'diff.file_b'),
1539 1578 ('@@', 'diff.hunk'),
1540 1579 ('-', 'diff.deleted'),
1541 1580 ('+', 'diff.inserted')]
1542 1581
1543 1582 for chunk in func(*args, **kw):
1544 1583 lines = chunk.split('\n')
1545 1584 for i, line in enumerate(lines):
1546 1585 if i != 0:
1547 1586 yield ('\n', '')
1548 1587 stripline = line
1549 1588 if line and line[0] in '+-':
1550 1589 # highlight trailing whitespace, but only in changed lines
1551 1590 stripline = line.rstrip()
1552 1591 for prefix, label in prefixes:
1553 1592 if stripline.startswith(prefix):
1554 1593 yield (stripline, label)
1555 1594 break
1556 1595 else:
1557 1596 yield (line, '')
1558 1597 if line != stripline:
1559 1598 yield (line[len(stripline):], 'diff.trailingwhitespace')
1560 1599
1561 1600 def diffui(*args, **kw):
1562 1601 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1563 1602 return difflabel(diff, *args, **kw)
1564 1603
1565 1604
1566 1605 def _addmodehdr(header, omode, nmode):
1567 1606 if omode != nmode:
1568 1607 header.append('old mode %s\n' % omode)
1569 1608 header.append('new mode %s\n' % nmode)
1570 1609
1571 1610 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1572 1611 copy, getfilectx, opts, losedatafn, prefix):
1573 1612
1574 1613 def join(f):
1575 1614 return os.path.join(prefix, f)
1576 1615
1577 1616 date1 = util.datestr(ctx1.date())
1578 1617 man1 = ctx1.manifest()
1579 1618
1580 1619 gone = set()
1581 1620 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1582 1621
1583 1622 copyto = dict([(v, k) for k, v in copy.items()])
1584 1623
1585 1624 if opts.git:
1586 1625 revs = None
1587 1626
1588 1627 for f in sorted(modified + added + removed):
1589 1628 to = None
1590 1629 tn = None
1591 1630 dodiff = True
1592 1631 header = []
1593 1632 if f in man1:
1594 1633 to = getfilectx(f, ctx1).data()
1595 1634 if f not in removed:
1596 1635 tn = getfilectx(f, ctx2).data()
1597 1636 a, b = f, f
1598 1637 if opts.git or losedatafn:
1599 1638 if f in added:
1600 1639 mode = gitmode[ctx2.flags(f)]
1601 1640 if f in copy or f in copyto:
1602 1641 if opts.git:
1603 1642 if f in copy:
1604 1643 a = copy[f]
1605 1644 else:
1606 1645 a = copyto[f]
1607 1646 omode = gitmode[man1.flags(a)]
1608 1647 _addmodehdr(header, omode, mode)
1609 1648 if a in removed and a not in gone:
1610 1649 op = 'rename'
1611 1650 gone.add(a)
1612 1651 else:
1613 1652 op = 'copy'
1614 1653 header.append('%s from %s\n' % (op, join(a)))
1615 1654 header.append('%s to %s\n' % (op, join(f)))
1616 1655 to = getfilectx(a, ctx1).data()
1617 1656 else:
1618 1657 losedatafn(f)
1619 1658 else:
1620 1659 if opts.git:
1621 1660 header.append('new file mode %s\n' % mode)
1622 1661 elif ctx2.flags(f):
1623 1662 losedatafn(f)
1624 1663 # In theory, if tn was copied or renamed we should check
1625 1664 # if the source is binary too but the copy record already
1626 1665 # forces git mode.
1627 1666 if util.binary(tn):
1628 1667 if opts.git:
1629 1668 dodiff = 'binary'
1630 1669 else:
1631 1670 losedatafn(f)
1632 1671 if not opts.git and not tn:
1633 1672 # regular diffs cannot represent new empty file
1634 1673 losedatafn(f)
1635 1674 elif f in removed:
1636 1675 if opts.git:
1637 1676 # have we already reported a copy above?
1638 1677 if ((f in copy and copy[f] in added
1639 1678 and copyto[copy[f]] == f) or
1640 1679 (f in copyto and copyto[f] in added
1641 1680 and copy[copyto[f]] == f)):
1642 1681 dodiff = False
1643 1682 else:
1644 1683 header.append('deleted file mode %s\n' %
1645 1684 gitmode[man1.flags(f)])
1646 1685 elif not to or util.binary(to):
1647 1686 # regular diffs cannot represent empty file deletion
1648 1687 losedatafn(f)
1649 1688 else:
1650 1689 oflag = man1.flags(f)
1651 1690 nflag = ctx2.flags(f)
1652 1691 binary = util.binary(to) or util.binary(tn)
1653 1692 if opts.git:
1654 1693 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1655 1694 if binary:
1656 1695 dodiff = 'binary'
1657 1696 elif binary or nflag != oflag:
1658 1697 losedatafn(f)
1659 1698 if opts.git:
1660 1699 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1661 1700
1662 1701 if dodiff:
1663 1702 if dodiff == 'binary':
1664 1703 text = b85diff(to, tn)
1665 1704 else:
1666 1705 text = mdiff.unidiff(to, date1,
1667 1706 # ctx2 date may be dynamic
1668 1707 tn, util.datestr(ctx2.date()),
1669 1708 join(a), join(b), revs, opts=opts)
1670 1709 if header and (text or len(header) > 1):
1671 1710 yield ''.join(header)
1672 1711 if text:
1673 1712 yield text
1674 1713
1675 1714 def diffstatdata(lines):
1676 1715 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1677 1716
1678 1717 filename, adds, removes = None, 0, 0
1679 1718 for line in lines:
1680 1719 if line.startswith('diff'):
1681 1720 if filename:
1682 1721 isbinary = adds == 0 and removes == 0
1683 1722 yield (filename, adds, removes, isbinary)
1684 1723 # set numbers to 0 anyway when starting new file
1685 1724 adds, removes = 0, 0
1686 1725 if line.startswith('diff --git'):
1687 1726 filename = gitre.search(line).group(1)
1688 1727 elif line.startswith('diff -r'):
1689 1728 # format: "diff -r ... -r ... filename"
1690 1729 filename = diffre.search(line).group(1)
1691 1730 elif line.startswith('+') and not line.startswith('+++'):
1692 1731 adds += 1
1693 1732 elif line.startswith('-') and not line.startswith('---'):
1694 1733 removes += 1
1695 1734 if filename:
1696 1735 isbinary = adds == 0 and removes == 0
1697 1736 yield (filename, adds, removes, isbinary)
1698 1737
1699 1738 def diffstat(lines, width=80, git=False):
1700 1739 output = []
1701 1740 stats = list(diffstatdata(lines))
1702 1741
1703 1742 maxtotal, maxname = 0, 0
1704 1743 totaladds, totalremoves = 0, 0
1705 1744 hasbinary = False
1706 1745
1707 1746 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1708 1747 for filename, adds, removes, isbinary in stats]
1709 1748
1710 1749 for filename, adds, removes, isbinary, namewidth in sized:
1711 1750 totaladds += adds
1712 1751 totalremoves += removes
1713 1752 maxname = max(maxname, namewidth)
1714 1753 maxtotal = max(maxtotal, adds + removes)
1715 1754 if isbinary:
1716 1755 hasbinary = True
1717 1756
1718 1757 countwidth = len(str(maxtotal))
1719 1758 if hasbinary and countwidth < 3:
1720 1759 countwidth = 3
1721 1760 graphwidth = width - countwidth - maxname - 6
1722 1761 if graphwidth < 10:
1723 1762 graphwidth = 10
1724 1763
1725 1764 def scale(i):
1726 1765 if maxtotal <= graphwidth:
1727 1766 return i
1728 1767 # If diffstat runs out of room it doesn't print anything,
1729 1768 # which isn't very useful, so always print at least one + or -
1730 1769 # if there were at least some changes.
1731 1770 return max(i * graphwidth // maxtotal, int(bool(i)))
1732 1771
1733 1772 for filename, adds, removes, isbinary, namewidth in sized:
1734 1773 if git and isbinary:
1735 1774 count = 'Bin'
1736 1775 else:
1737 1776 count = adds + removes
1738 1777 pluses = '+' * scale(adds)
1739 1778 minuses = '-' * scale(removes)
1740 1779 output.append(' %s%s | %*s %s%s\n' %
1741 1780 (filename, ' ' * (maxname - namewidth),
1742 1781 countwidth, count,
1743 1782 pluses, minuses))
1744 1783
1745 1784 if stats:
1746 1785 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1747 1786 % (len(stats), totaladds, totalremoves))
1748 1787
1749 1788 return ''.join(output)
1750 1789
1751 1790 def diffstatui(*args, **kw):
1752 1791 '''like diffstat(), but yields 2-tuples of (output, label) for
1753 1792 ui.write()
1754 1793 '''
1755 1794
1756 1795 for line in diffstat(*args, **kw).splitlines():
1757 1796 if line and line[-1] in '+-':
1758 1797 name, graph = line.rsplit(' ', 1)
1759 1798 yield (name + ' ', '')
1760 1799 m = re.search(r'\++', graph)
1761 1800 if m:
1762 1801 yield (m.group(0), 'diffstat.inserted')
1763 1802 m = re.search(r'-+', graph)
1764 1803 if m:
1765 1804 yield (m.group(0), 'diffstat.deleted')
1766 1805 else:
1767 1806 yield (line, '')
1768 1807 yield ('\n', '')
@@ -1,149 +1,148 b''
1 1
2 2 Issue835: qpush fails immediately when patching a missing file, but
3 3 remaining added files are still created empty which will trick a
4 4 future qrefresh.
5 5
6 6 $ cat > writelines.py <<EOF
7 7 > import sys
8 8 > path = sys.argv[1]
9 9 > args = sys.argv[2:]
10 10 > assert (len(args) % 2) == 0
11 11 >
12 12 > f = file(path, 'wb')
13 13 > for i in xrange(len(args)/2):
14 14 > count, s = args[2*i:2*i+2]
15 15 > count = int(count)
16 16 > s = s.decode('string_escape')
17 17 > f.write(s*count)
18 18 > f.close()
19 19 > EOF
20 20
21 21 $ echo "[extensions]" >> $HGRCPATH
22 22 $ echo "mq=" >> $HGRCPATH
23 23
24 24 $ hg init normal
25 25 $ cd normal
26 26 $ python ../writelines.py b 10 'a\n'
27 27 $ hg ci -Am addb
28 28 adding b
29 29 $ echo a > a
30 30 $ python ../writelines.py b 2 'b\n' 10 'a\n' 2 'c\n'
31 31 $ echo c > c
32 32 $ hg add a c
33 33 $ hg qnew -f changeb
34 34 $ hg qpop
35 35 popping changeb
36 36 patch queue now empty
37 37 $ hg rm b
38 38 $ hg ci -Am rmb
39 39
40 40 Push patch with missing target:
41 41
42 42 $ hg qpush
43 43 applying changeb
44 44 unable to find 'b' for patching
45 45 2 out of 2 hunks FAILED -- saving rejects to file b.rej
46 46 patch failed, unable to continue (try -v)
47 47 patch failed, rejects left in working dir
48 48 errors during apply, please fix and refresh changeb
49 49 [2]
50 50
51 51 Display added files:
52 52
53 53 $ cat a
54 54 a
55 55 $ cat c
56 56 c
57 57
58 58 Display rejections:
59 59
60 60 $ cat b.rej
61 61 --- b
62 62 +++ b
63 63 @@ -1,3 +1,5 @@
64 64 +b
65 65 +b
66 66 a
67 67 a
68 68 a
69 69 @@ -8,3 +10,5 @@
70 70 a
71 71 a
72 72 a
73 73 +c
74 74 +c
75 75
76 76 $ cd ..
77 77
78 78
79 79 $ echo "[diff]" >> $HGRCPATH
80 80 $ echo "git=1" >> $HGRCPATH
81 81
82 82 $ hg init git
83 83 $ cd git
84 84 $ python ../writelines.py b 1 '\x00'
85 85 $ hg ci -Am addb
86 86 adding b
87 87 $ echo a > a
88 88 $ python ../writelines.py b 1 '\x01' 1 '\x00'
89 89 $ echo c > c
90 90 $ hg add a c
91 91 $ hg qnew -f changeb
92 92 $ hg qpop
93 93 popping changeb
94 94 patch queue now empty
95 95 $ hg rm b
96 96 $ hg ci -Am rmb
97 97
98 98 Push git patch with missing target:
99 99
100 100 $ hg qpush
101 101 applying changeb
102 102 unable to find 'b' for patching
103 103 1 out of 1 hunks FAILED -- saving rejects to file b.rej
104 b: No such file or directory
105 104 patch failed, unable to continue (try -v)
106 105 patch failed, rejects left in working dir
107 106 errors during apply, please fix and refresh changeb
108 107 [2]
109 108 $ hg st
110 109 ? b.rej
111 110
112 111 Display added files:
113 112
114 113 $ cat a
115 114 a
116 115 $ cat c
117 116 c
118 117
119 118 Display rejections:
120 119
121 120 $ cat b.rej
122 121 --- b
123 122 +++ b
124 123 GIT binary patch
125 124 literal 2
126 125 Jc${No0000400IC2
127 126
128 127 $ cd ..
129 128
130 129 Test push creating directory during git copy or rename:
131 130
132 131 $ hg init missingdir
133 132 $ cd missingdir
134 133 $ echo a > a
135 134 $ hg ci -Am adda
136 135 adding a
137 136 $ mkdir d
138 137 $ hg copy a d/a2
139 138 $ hg mv a d/a
140 139 $ hg qnew -g -f patch
141 140 $ hg qpop
142 141 popping patch
143 142 patch queue now empty
144 143 $ hg qpush
145 144 applying patch
146 145 now at: patch
147 146
148 147 $ cd ..
149 148
General Comments 0
You need to be logged in to leave comments. Login now