comparison mercurial/patch.py @ 6042:2da5b19a6460

Merge with crew
author Bryan O'Sullivan <bos@serpentine.com>
date Wed, 06 Feb 2008 19:57:52 -0800
parents 1d0bfa4c75c0 d39af2eabb8c
children 36ab165abbe2
comparison
equal deleted inserted replaced
6041:dd714452c26e 6042:2da5b19a6460
7 # of the GNU General Public License, incorporated herein by reference. 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 from i18n import _ 9 from i18n import _
10 from node import * 10 from node import *
11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers 11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
12 import cStringIO, email.Parser, os, popen2, re, sha 12 import cStringIO, email.Parser, os, popen2, re, sha, errno
13 import sys, tempfile, zlib 13 import sys, tempfile, zlib
14 14
15 class PatchError(Exception): 15 class PatchError(Exception):
16 pass 16 pass
17 17
57 try: 57 try:
58 msg = email.Parser.Parser().parse(fileobj) 58 msg = email.Parser.Parser().parse(fileobj)
59 59
60 subject = msg['Subject'] 60 subject = msg['Subject']
61 user = msg['From'] 61 user = msg['From']
62 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
62 # should try to parse msg['Date'] 63 # should try to parse msg['Date']
63 date = None 64 date = None
64 nodeid = None 65 nodeid = None
65 branch = None 66 branch = None
66 parents = [] 67 parents = []
109 branch = line[9:] 110 branch = line[9:]
110 elif line.startswith("# Node ID "): 111 elif line.startswith("# Node ID "):
111 nodeid = line[10:] 112 nodeid = line[10:]
112 elif line.startswith("# Parent "): 113 elif line.startswith("# Parent "):
113 parents.append(line[10:]) 114 parents.append(line[10:])
114 elif line == '---' and 'git-send-email' in msg['X-Mailer']: 115 elif line == '---' and gitsendmail:
115 ignoretext = True 116 ignoretext = True
116 if not line.startswith('# ') and not ignoretext: 117 if not line.startswith('# ') and not ignoretext:
117 cfp.write(line) 118 cfp.write(line)
118 cfp.write('\n') 119 cfp.write('\n')
119 message = cfp.getvalue() 120 message = cfp.getvalue()
140 141
141 GP_PATCH = 1 << 0 # we have to run patch 142 GP_PATCH = 1 << 0 # we have to run patch
142 GP_FILTER = 1 << 1 # there's some copy/rename operation 143 GP_FILTER = 1 << 1 # there's some copy/rename operation
143 GP_BINARY = 1 << 2 # there's a binary patch 144 GP_BINARY = 1 << 2 # there's a binary patch
144 145
145 def readgitpatch(fp, firstline): 146 def readgitpatch(fp, firstline=None):
146 """extract git-style metadata about patches from <patchname>""" 147 """extract git-style metadata about patches from <patchname>"""
147 class gitpatch: 148 class gitpatch:
148 "op is one of ADD, DELETE, RENAME, MODIFY or COPY" 149 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
149 def __init__(self, path): 150 def __init__(self, path):
150 self.path = path 151 self.path = path
151 self.oldpath = None 152 self.oldpath = None
152 self.mode = None 153 self.mode = None
153 self.op = 'MODIFY' 154 self.op = 'MODIFY'
154 self.copymod = False
155 self.lineno = 0 155 self.lineno = 0
156 self.binary = False 156 self.binary = False
157 157
158 def reader(fp, firstline): 158 def reader(fp, firstline):
159 yield firstline 159 if firstline is not None:
160 yield firstline
160 for line in fp: 161 for line in fp:
161 yield line 162 yield line
162 163
163 # Filter patch for git information 164 # Filter patch for git information
164 gitre = re.compile('diff --git a/(.*) b/(.*)') 165 gitre = re.compile('diff --git a/(.*) b/(.*)')
179 gp = gitpatch(dst) 180 gp = gitpatch(dst)
180 gp.lineno = lineno 181 gp.lineno = lineno
181 elif gp: 182 elif gp:
182 if line.startswith('--- '): 183 if line.startswith('--- '):
183 if gp.op in ('COPY', 'RENAME'): 184 if gp.op in ('COPY', 'RENAME'):
184 gp.copymod = True
185 dopatch |= GP_FILTER 185 dopatch |= GP_FILTER
186 gitpatches.append(gp) 186 gitpatches.append(gp)
187 gp = None 187 gp = None
188 dopatch |= GP_PATCH 188 dopatch |= GP_PATCH
189 continue 189 continue
199 gp.path = line[8:].rstrip() 199 gp.path = line[8:].rstrip()
200 elif line.startswith('deleted file'): 200 elif line.startswith('deleted file'):
201 gp.op = 'DELETE' 201 gp.op = 'DELETE'
202 elif line.startswith('new file mode '): 202 elif line.startswith('new file mode '):
203 gp.op = 'ADD' 203 gp.op = 'ADD'
204 gp.mode = int(line.rstrip()[-3:], 8) 204 gp.mode = int(line.rstrip()[-6:], 8)
205 elif line.startswith('new mode '): 205 elif line.startswith('new mode '):
206 gp.mode = int(line.rstrip()[-3:], 8) 206 gp.mode = int(line.rstrip()[-6:], 8)
207 elif line.startswith('GIT binary patch'): 207 elif line.startswith('GIT binary patch'):
208 dopatch |= GP_BINARY 208 dopatch |= GP_BINARY
209 gp.binary = True 209 gp.binary = True
210 if gp: 210 if gp:
211 gitpatches.append(gp) 211 gitpatches.append(gp)
247 returns whether patch was applied with fuzz factor.""" 247 returns whether patch was applied with fuzz factor."""
248 248
249 fuzz = False 249 fuzz = False
250 if cwd: 250 if cwd:
251 args.append('-d %s' % util.shellquote(cwd)) 251 args.append('-d %s' % util.shellquote(cwd))
252 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, 252 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
253 util.shellquote(patchname))) 253 util.shellquote(patchname)))
254 254
255 for line in fp: 255 for line in fp:
256 line = line.rstrip() 256 line = line.rstrip()
257 ui.note(line + '\n') 257 ui.note(line + '\n')
276 if code: 276 if code:
277 raise PatchError(_("patch command failed: %s") % 277 raise PatchError(_("patch command failed: %s") %
278 util.explain_exit(code)[0]) 278 util.explain_exit(code)[0])
279 return fuzz 279 return fuzz
280 280
281 def internalpatch(patchname, ui, strip, cwd, files): 281 def internalpatch(patchobj, ui, strip, cwd, files={}):
282 """use builtin patch to apply <patchname> to the working directory. 282 """use builtin patch to apply <patchobj> to the working directory.
283 returns whether patch was applied with fuzz factor.""" 283 returns whether patch was applied with fuzz factor."""
284 fp = file(patchname, 'rb') 284 try:
285 fp = file(patchobj, 'rb')
286 except TypeError:
287 fp = patchobj
285 if cwd: 288 if cwd:
286 curdir = os.getcwd() 289 curdir = os.getcwd()
287 os.chdir(cwd) 290 os.chdir(cwd)
288 try: 291 try:
289 ret = applydiff(ui, fp, files, strip=strip) 292 ret = applydiff(ui, fp, files, strip=strip)
297 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1 300 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
298 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@') 301 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
299 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)') 302 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
300 303
301 class patchfile: 304 class patchfile:
302 def __init__(self, ui, fname): 305 def __init__(self, ui, fname, missing=False):
303 self.fname = fname 306 self.fname = fname
304 self.ui = ui 307 self.ui = ui
305 try: 308 self.lines = []
306 fp = file(fname, 'rb') 309 self.exists = False
307 self.lines = fp.readlines() 310 self.missing = missing
308 self.exists = True 311 if not missing:
309 except IOError: 312 try:
313 fp = file(fname, 'rb')
314 self.lines = fp.readlines()
315 self.exists = True
316 except IOError:
317 pass
318 else:
319 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
320
321 if not self.exists:
310 dirname = os.path.dirname(fname) 322 dirname = os.path.dirname(fname)
311 if dirname and not os.path.isdir(dirname): 323 if dirname and not os.path.isdir(dirname):
312 dirs = dirname.split(os.path.sep) 324 os.makedirs(dirname)
313 d = "" 325
314 for x in dirs:
315 d = os.path.join(d, x)
316 if not os.path.isdir(d):
317 os.mkdir(d)
318 self.lines = []
319 self.exists = False
320
321 self.hash = {} 326 self.hash = {}
322 self.dirty = 0 327 self.dirty = 0
323 self.offset = 0 328 self.offset = 0
324 self.rej = [] 329 self.rej = []
325 self.fileprinted = False 330 self.fileprinted = False
344 # from linenum 349 # from linenum
345 def sorter(a, b): 350 def sorter(a, b):
346 vala = abs(a - linenum) 351 vala = abs(a - linenum)
347 valb = abs(b - linenum) 352 valb = abs(b - linenum)
348 return cmp(vala, valb) 353 return cmp(vala, valb)
349 354
350 try: 355 try:
351 cand = self.hash[l] 356 cand = self.hash[l]
352 except: 357 except:
353 return [] 358 return []
354 359
355 if len(cand) > 1: 360 if len(cand) > 1:
356 # resort our list of potentials forward then back. 361 # resort our list of potentials forward then back.
357 cand.sort(cmp=sorter) 362 cand.sort(sorter)
358 return cand 363 return cand
359 364
360 def hashlines(self): 365 def hashlines(self):
361 self.hash = {} 366 self.hash = {}
362 for x in xrange(len(self.lines)): 367 for x in xrange(len(self.lines)):
397 if not dest: 402 if not dest:
398 dest = self.fname 403 dest = self.fname
399 st = None 404 st = None
400 try: 405 try:
401 st = os.lstat(dest) 406 st = os.lstat(dest)
402 if st.st_nlink > 1: 407 except OSError, inst:
403 os.unlink(dest) 408 if inst.errno != errno.ENOENT:
404 except: pass 409 raise
410 if st and st.st_nlink > 1:
411 os.unlink(dest)
405 fp = file(dest, 'wb') 412 fp = file(dest, 'wb')
406 if st: 413 if st and st.st_nlink > 1:
407 os.chmod(dest, st.st_mode) 414 os.chmod(dest, st.st_mode)
408 fp.writelines(self.lines) 415 fp.writelines(self.lines)
409 fp.close() 416 fp.close()
410 417
411 def close(self): 418 def close(self):
419 h.lenb)) 426 h.lenb))
420 427
421 self.hunks += 1 428 self.hunks += 1
422 if reverse: 429 if reverse:
423 h.reverse() 430 h.reverse()
431
432 if self.missing:
433 self.rej.append(h)
434 return -1
424 435
425 if self.exists and h.createfile(): 436 if self.exists and h.createfile():
426 self.ui.warn(_("file %s already exists\n") % self.fname) 437 self.ui.warn(_("file %s already exists\n") % self.fname)
427 self.rej.append(h) 438 self.rej.append(h)
428 return -1 439 return -1
492 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start)) 503 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
493 self.rej.append(h) 504 self.rej.append(h)
494 return -1 505 return -1
495 506
496 class hunk: 507 class hunk:
497 def __init__(self, desc, num, lr, context): 508 def __init__(self, desc, num, lr, context, gitpatch=None):
498 self.number = num 509 self.number = num
499 self.desc = desc 510 self.desc = desc
500 self.hunk = [ desc ] 511 self.hunk = [ desc ]
501 self.a = [] 512 self.a = []
502 self.b = [] 513 self.b = []
503 if context: 514 if context:
504 self.read_context_hunk(lr) 515 self.read_context_hunk(lr)
505 else: 516 else:
506 self.read_unified_hunk(lr) 517 self.read_unified_hunk(lr)
518 self.gitpatch = gitpatch
507 519
508 def read_unified_hunk(self, lr): 520 def read_unified_hunk(self, lr):
509 m = unidesc.match(self.desc) 521 m = unidesc.match(self.desc)
510 if not m: 522 if not m:
511 raise PatchError(_("bad hunk #%d") % self.number) 523 raise PatchError(_("bad hunk #%d") % self.number)
656 668
657 def complete(self): 669 def complete(self):
658 return len(self.a) == self.lena and len(self.b) == self.lenb 670 return len(self.a) == self.lena and len(self.b) == self.lenb
659 671
660 def createfile(self): 672 def createfile(self):
661 return self.starta == 0 and self.lena == 0 673 create = self.gitpatch is None or self.gitpatch.op == 'ADD'
674 return self.starta == 0 and self.lena == 0 and create
662 675
663 def rmfile(self): 676 def rmfile(self):
664 return self.startb == 0 and self.lenb == 0 677 remove = self.gitpatch is None or self.gitpatch.op == 'DELETE'
678 return self.startb == 0 and self.lenb == 0 and remove
665 679
666 def fuzzit(self, l, fuzz, toponly): 680 def fuzzit(self, l, fuzz, toponly):
667 # this removes context lines from the top and bottom of list 'l'. It 681 # this removes context lines from the top and bottom of list 'l'. It
668 # checks the hunk to make sure only context lines are removed, and then 682 # checks the hunk to make sure only context lines are removed, and then
669 # returns a new shortened list of lines. 683 # returns a new shortened list of lines.
700 return l[top:len(l)-bot] 714 return l[top:len(l)-bot]
701 return l 715 return l
702 716
703 def old(self, fuzz=0, toponly=False): 717 def old(self, fuzz=0, toponly=False):
704 return self.fuzzit(self.a, fuzz, toponly) 718 return self.fuzzit(self.a, fuzz, toponly)
705 719
706 def newctrl(self): 720 def newctrl(self):
707 res = [] 721 res = []
708 for x in self.hunk: 722 for x in self.hunk:
709 c = x[0] 723 c = x[0]
710 if c == ' ' or c == '+': 724 if c == ' ' or c == '+':
760 len(text), size) 774 len(text), size)
761 self.text = text 775 self.text = text
762 776
763 def parsefilename(str): 777 def parsefilename(str):
764 # --- filename \t|space stuff 778 # --- filename \t|space stuff
765 s = str[4:] 779 s = str[4:].rstrip('\r\n')
766 i = s.find('\t') 780 i = s.find('\t')
767 if i < 0: 781 if i < 0:
768 i = s.find(' ') 782 i = s.find(' ')
769 if i < 0: 783 if i < 0:
770 return s 784 return s
789 return path[i:].rstrip() 803 return path[i:].rstrip()
790 804
791 nulla = afile_orig == "/dev/null" 805 nulla = afile_orig == "/dev/null"
792 nullb = bfile_orig == "/dev/null" 806 nullb = bfile_orig == "/dev/null"
793 afile = pathstrip(afile_orig, strip) 807 afile = pathstrip(afile_orig, strip)
794 gooda = os.path.exists(afile) and not nulla 808 gooda = not nulla and os.path.exists(afile)
795 bfile = pathstrip(bfile_orig, strip) 809 bfile = pathstrip(bfile_orig, strip)
796 if afile == bfile: 810 if afile == bfile:
797 goodb = gooda 811 goodb = gooda
798 else: 812 else:
799 goodb = os.path.exists(bfile) and not nullb 813 goodb = not nullb and os.path.exists(bfile)
800 createfunc = hunk.createfile 814 createfunc = hunk.createfile
801 if reverse: 815 if reverse:
802 createfunc = hunk.rmfile 816 createfunc = hunk.rmfile
803 if not goodb and not gooda and not createfunc(): 817 missing = not goodb and not gooda and not createfunc()
804 raise PatchError(_("unable to find %s or %s for patching") % 818 fname = None
805 (afile, bfile)) 819 if not missing:
806 if gooda and goodb: 820 if gooda and goodb:
807 fname = bfile 821 fname = (afile in bfile) and afile or bfile
808 if afile in bfile: 822 elif gooda:
809 fname = afile 823 fname = afile
810 elif gooda: 824
811 fname = afile 825 if not fname:
812 elif not nullb: 826 if not nullb:
813 fname = bfile 827 fname = (afile in bfile) and afile or bfile
814 if afile in bfile: 828 elif not nulla:
815 fname = afile 829 fname = afile
816 elif not nulla: 830 else:
817 fname = afile 831 raise PatchError(_("undefined source and destination files"))
818 return fname 832
833 return fname, missing
819 834
820 class linereader: 835 class linereader:
821 # simple class to allow pushing lines back into the input stream 836 # simple class to allow pushing lines back into the input stream
822 def __init__(self, fp): 837 def __init__(self, fp):
823 self.fp = fp 838 self.fp = fp
831 l = self.buf[0] 846 l = self.buf[0]
832 del self.buf[0] 847 del self.buf[0]
833 return l 848 return l
834 return self.fp.readline() 849 return self.fp.readline()
835 850
836 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False, 851 def iterhunks(ui, fp, sourcefile=None):
837 rejmerge=None, updatedir=None): 852 """Read a patch and yield the following events:
838 """reads a patch from fp and tries to apply it. The dict 'changed' is 853 - ("file", afile, bfile, firsthunk): select a new target file.
839 filled in with all of the filenames changed by the patch. Returns 0 854 - ("hunk", hunk): a new hunk is ready to be applied, follows a
840 for a clean patch, -1 if any rejects were found and 1 if there was 855 "file" event.
841 any fuzz.""" 856 - ("git", gitchanges): current diff is in git format, gitchanges
842 857 maps filenames to gitpatch records. Unique event.
843 def scangitpatch(fp, firstline, cwd=None): 858 """
859
860 def scangitpatch(fp, firstline):
844 '''git patches can modify a file, then copy that file to 861 '''git patches can modify a file, then copy that file to
845 a new file, but expect the source to be the unmodified form. 862 a new file, but expect the source to be the unmodified form.
846 So we scan the patch looking for that case so we can do 863 So we scan the patch looking for that case so we can do
847 the copies ahead of time.''' 864 the copies ahead of time.'''
848 865
851 pos = fp.tell() 868 pos = fp.tell()
852 except IOError: 869 except IOError:
853 fp = cStringIO.StringIO(fp.read()) 870 fp = cStringIO.StringIO(fp.read())
854 871
855 (dopatch, gitpatches) = readgitpatch(fp, firstline) 872 (dopatch, gitpatches) = readgitpatch(fp, firstline)
856 for gp in gitpatches:
857 if gp.copymod:
858 copyfile(gp.oldpath, gp.path, basedir=cwd)
859
860 fp.seek(pos) 873 fp.seek(pos)
861 874
862 return fp, dopatch, gitpatches 875 return fp, dopatch, gitpatches
863 876
877 changed = {}
864 current_hunk = None 878 current_hunk = None
865 current_file = None
866 afile = "" 879 afile = ""
867 bfile = "" 880 bfile = ""
868 state = None 881 state = None
869 hunknum = 0 882 hunknum = 0
870 rejects = 0 883 emitfile = False
871 884
872 git = False 885 git = False
873 gitre = re.compile('diff --git (a/.*) (b/.*)') 886 gitre = re.compile('diff --git (a/.*) (b/.*)')
874 887
875 # our states 888 # our states
876 BFILE = 1 889 BFILE = 1
877 err = 0
878 context = None 890 context = None
879 lr = linereader(fp) 891 lr = linereader(fp)
880 dopatch = True 892 dopatch = True
881 gitworkdone = False 893 gitworkdone = False
882 894
886 if not x: 898 if not x:
887 break 899 break
888 if current_hunk: 900 if current_hunk:
889 if x.startswith('\ '): 901 if x.startswith('\ '):
890 current_hunk.fix_newline() 902 current_hunk.fix_newline()
891 ret = current_file.apply(current_hunk, reverse) 903 yield 'hunk', current_hunk
892 if ret >= 0:
893 changed.setdefault(current_file.fname, (None, None))
894 if ret > 0:
895 err = 1
896 current_hunk = None 904 current_hunk = None
897 gitworkdone = False 905 gitworkdone = False
898 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or 906 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
899 ((context or context == None) and x.startswith('***************')))): 907 ((context or context == None) and x.startswith('***************')))):
900 try: 908 try:
901 if context == None and x.startswith('***************'): 909 if context == None and x.startswith('***************'):
902 context = True 910 context = True
903 current_hunk = hunk(x, hunknum + 1, lr, context) 911 gpatch = changed.get(bfile[2:], (None, None))[1]
912 current_hunk = hunk(x, hunknum + 1, lr, context, gpatch)
904 except PatchError, err: 913 except PatchError, err:
905 ui.debug(err) 914 ui.debug(err)
906 current_hunk = None 915 current_hunk = None
907 continue 916 continue
908 hunknum += 1 917 hunknum += 1
909 if not current_file: 918 if emitfile:
910 if sourcefile: 919 emitfile = False
911 current_file = patchfile(ui, sourcefile) 920 yield 'file', (afile, bfile, current_hunk)
912 else:
913 current_file = selectfile(afile, bfile, current_hunk,
914 strip, reverse)
915 current_file = patchfile(ui, current_file)
916 elif state == BFILE and x.startswith('GIT binary patch'): 921 elif state == BFILE and x.startswith('GIT binary patch'):
917 current_hunk = binhunk(changed[bfile[2:]][1]) 922 current_hunk = binhunk(changed[bfile[2:]][1])
918 if not current_file:
919 if sourcefile:
920 current_file = patchfile(ui, sourcefile)
921 else:
922 current_file = selectfile(afile, bfile, current_hunk,
923 strip, reverse)
924 current_file = patchfile(ui, current_file)
925 hunknum += 1 923 hunknum += 1
924 if emitfile:
925 emitfile = False
926 yield 'file', (afile, bfile, current_hunk)
926 current_hunk.extract(fp) 927 current_hunk.extract(fp)
927 elif x.startswith('diff --git'): 928 elif x.startswith('diff --git'):
928 # check for git diff, scanning the whole patch file if needed 929 # check for git diff, scanning the whole patch file if needed
929 m = gitre.match(x) 930 m = gitre.match(x)
930 if m: 931 if m:
931 afile, bfile = m.group(1, 2) 932 afile, bfile = m.group(1, 2)
932 if not git: 933 if not git:
933 git = True 934 git = True
934 fp, dopatch, gitpatches = scangitpatch(fp, x) 935 fp, dopatch, gitpatches = scangitpatch(fp, x)
936 yield 'git', gitpatches
935 for gp in gitpatches: 937 for gp in gitpatches:
936 changed[gp.path] = (gp.op, gp) 938 changed[gp.path] = (gp.op, gp)
937 # else error? 939 # else error?
938 # copy/rename + modify should modify target, not source 940 # copy/rename + modify should modify target, not source
939 if changed.get(bfile[2:], (None, None))[0] in ('COPY', 941 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
966 context = True 968 context = True
967 afile = parsefilename(x) 969 afile = parsefilename(x)
968 bfile = parsefilename(l2) 970 bfile = parsefilename(l2)
969 971
970 if newfile: 972 if newfile:
971 if current_file: 973 emitfile = True
972 current_file.close()
973 if rejmerge:
974 rejmerge(current_file)
975 rejects += len(current_file.rej)
976 state = BFILE 974 state = BFILE
977 current_file = None
978 hunknum = 0 975 hunknum = 0
979 if current_hunk: 976 if current_hunk:
980 if current_hunk.complete(): 977 if current_hunk.complete():
978 yield 'hunk', current_hunk
979 else:
980 raise PatchError(_("malformed patch %s %s") % (afile,
981 current_hunk.desc))
982
983 if hunknum == 0 and dopatch and not gitworkdone:
984 raise NoHunks
985
986 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
987 rejmerge=None, updatedir=None):
988 """reads a patch from fp and tries to apply it. The dict 'changed' is
989 filled in with all of the filenames changed by the patch. Returns 0
990 for a clean patch, -1 if any rejects were found and 1 if there was
991 any fuzz."""
992
993 rejects = 0
994 err = 0
995 current_file = None
996 gitpatches = None
997
998 def closefile():
999 if not current_file:
1000 return 0
1001 current_file.close()
1002 if rejmerge:
1003 rejmerge(current_file)
1004 return len(current_file.rej)
1005
1006 for state, values in iterhunks(ui, fp, sourcefile):
1007 if state == 'hunk':
1008 if not current_file:
1009 continue
1010 current_hunk = values
981 ret = current_file.apply(current_hunk, reverse) 1011 ret = current_file.apply(current_hunk, reverse)
982 if ret >= 0: 1012 if ret >= 0:
983 changed.setdefault(current_file.fname, (None, None)) 1013 changed.setdefault(current_file.fname, (None, None))
984 if ret > 0: 1014 if ret > 0:
985 err = 1 1015 err = 1
1016 elif state == 'file':
1017 rejects += closefile()
1018 afile, bfile, first_hunk = values
1019 try:
1020 if sourcefile:
1021 current_file = patchfile(ui, sourcefile)
1022 else:
1023 current_file, missing = selectfile(afile, bfile, first_hunk,
1024 strip, reverse)
1025 current_file = patchfile(ui, current_file, missing)
1026 except PatchError, err:
1027 ui.warn(str(err) + '\n')
1028 current_file, current_hunk = None, None
1029 rejects += 1
1030 continue
1031 elif state == 'git':
1032 gitpatches = values
1033 for gp in gitpatches:
1034 if gp.op in ('COPY', 'RENAME'):
1035 copyfile(gp.oldpath, gp.path)
1036 changed[gp.path] = (gp.op, gp)
986 else: 1037 else:
987 fname = current_file and current_file.fname or None 1038 raise util.Abort(_('unsupported parser state: %s') % state)
988 raise PatchError(_("malformed patch %s %s") % (fname, 1039
989 current_hunk.desc)) 1040 rejects += closefile()
990 if current_file: 1041
991 current_file.close() 1042 if updatedir and gitpatches:
992 if rejmerge:
993 rejmerge(current_file)
994 rejects += len(current_file.rej)
995 if updatedir and git:
996 updatedir(gitpatches) 1043 updatedir(gitpatches)
997 if rejects: 1044 if rejects:
998 return -1 1045 return -1
999 if hunknum == 0 and dopatch and not gitworkdone:
1000 raise NoHunks
1001 return err 1046 return err
1002 1047
1003 def diffopts(ui, opts={}, untrusted=False): 1048 def diffopts(ui, opts={}, untrusted=False):
1004 def get(key, name=None): 1049 def get(key, name=None):
1005 return (opts.get(key) or 1050 return (opts.get(key) or
1025 if cwd: 1070 if cwd:
1026 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()] 1071 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1027 for f in patches: 1072 for f in patches:
1028 ctype, gp = patches[f] 1073 ctype, gp = patches[f]
1029 if ctype == 'RENAME': 1074 if ctype == 'RENAME':
1030 copies.append((gp.oldpath, gp.path, gp.copymod)) 1075 copies.append((gp.oldpath, gp.path))
1031 removes[gp.oldpath] = 1 1076 removes[gp.oldpath] = 1
1032 elif ctype == 'COPY': 1077 elif ctype == 'COPY':
1033 copies.append((gp.oldpath, gp.path, gp.copymod)) 1078 copies.append((gp.oldpath, gp.path))
1034 elif ctype == 'DELETE': 1079 elif ctype == 'DELETE':
1035 removes[gp.path] = 1 1080 removes[gp.path] = 1
1036 for src, dst, after in copies: 1081 for src, dst in copies:
1037 if not after:
1038 copyfile(src, dst, repo.root)
1039 repo.copy(src, dst) 1082 repo.copy(src, dst)
1040 removes = removes.keys() 1083 removes = removes.keys()
1041 if removes: 1084 if removes:
1042 removes.sort() 1085 removes.sort()
1043 repo.remove(removes, True) 1086 repo.remove(removes, True)
1044 for f in patches: 1087 for f in patches:
1045 ctype, gp = patches[f] 1088 ctype, gp = patches[f]
1046 if gp and gp.mode: 1089 if gp and gp.mode:
1047 x = gp.mode & 0100 != 0 1090 flags = ''
1091 if gp.mode & 0100:
1092 flags = 'x'
1093 elif gp.mode & 020000:
1094 flags = 'l'
1048 dst = os.path.join(repo.root, gp.path) 1095 dst = os.path.join(repo.root, gp.path)
1049 # patch won't create empty files 1096 # patch won't create empty files
1050 if ctype == 'ADD' and not os.path.exists(dst): 1097 if ctype == 'ADD' and not os.path.exists(dst):
1051 repo.wwrite(gp.path, '', x and 'x' or '') 1098 repo.wwrite(gp.path, '', flags)
1052 else: 1099 else:
1053 util.set_exec(dst, x) 1100 util.set_flags(dst, flags)
1054 cmdutil.addremove(repo, cfiles) 1101 cmdutil.addremove(repo, cfiles)
1055 files = patches.keys() 1102 files = patches.keys()
1056 files.extend([r for r in removes if r not in files]) 1103 files.extend([r for r in removes if r not in files])
1057 files.sort() 1104 files.sort()
1058 1105
1059 return files 1106 return files
1060 1107
1061 def b85diff(fp, to, tn): 1108 def b85diff(to, tn):
1062 '''print base85-encoded binary diff''' 1109 '''print base85-encoded binary diff'''
1063 def gitindex(text): 1110 def gitindex(text):
1064 if not text: 1111 if not text:
1065 return '0' * 40 1112 return '0' * 40
1066 l = len(text) 1113 l = len(text)
1140 return 1187 return
1141 1188
1142 if node2: 1189 if node2:
1143 ctx2 = context.changectx(repo, node2) 1190 ctx2 = context.changectx(repo, node2)
1144 execf2 = ctx2.manifest().execf 1191 execf2 = ctx2.manifest().execf
1192 linkf2 = ctx2.manifest().linkf
1145 else: 1193 else:
1146 ctx2 = context.workingctx(repo) 1194 ctx2 = context.workingctx(repo)
1147 execf2 = util.execfunc(repo.root, None) 1195 execf2 = util.execfunc(repo.root, None)
1196 linkf2 = util.linkfunc(repo.root, None)
1148 if execf2 is None: 1197 if execf2 is None:
1149 execf2 = ctx2.parents()[0].manifest().copy().execf 1198 mc = ctx2.parents()[0].manifest().copy()
1199 execf2 = mc.execf
1200 linkf2 = mc.linkf
1150 1201
1151 # returns False if there was no rename between ctx1 and ctx2 1202 # returns False if there was no rename between ctx1 and ctx2
1152 # returns None if the file was created between ctx1 and ctx2 1203 # returns None if the file was created between ctx1 and ctx2
1153 # returns the (file, node) present in ctx1 that was renamed to f in ctx2 1204 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1154 def renamed(f): 1205 # This will only really work if c1 is the Nth 1st parent of c2.
1155 startrev = ctx1.rev() 1206 def renamed(c1, c2, man, f):
1156 c = ctx2 1207 startrev = c1.rev()
1208 c = c2
1157 crev = c.rev() 1209 crev = c.rev()
1158 if crev is None: 1210 if crev is None:
1159 crev = repo.changelog.count() 1211 crev = repo.changelog.count()
1160 orig = f 1212 orig = f
1213 files = (f,)
1161 while crev > startrev: 1214 while crev > startrev:
1162 if f in c.files(): 1215 if f in files:
1163 try: 1216 try:
1164 src = getfilectx(f, c).renamed() 1217 src = getfilectx(f, c).renamed()
1165 except revlog.LookupError: 1218 except revlog.LookupError:
1166 return None 1219 return None
1167 if src: 1220 if src:
1168 f = src[0] 1221 f = src[0]
1169 crev = c.parents()[0].rev() 1222 crev = c.parents()[0].rev()
1170 # try to reuse 1223 # try to reuse
1171 c = getctx(crev) 1224 c = getctx(crev)
1172 if f not in man1: 1225 files = c.files()
1226 if f not in man:
1173 return None 1227 return None
1174 if f == orig: 1228 if f == orig:
1175 return False 1229 return False
1176 return f 1230 return f
1177 1231
1181 hexfunc = repo.ui.debugflag and hex or short 1235 hexfunc = repo.ui.debugflag and hex or short
1182 r = [hexfunc(node) for node in [node1, node2] if node] 1236 r = [hexfunc(node) for node in [node1, node2] if node]
1183 1237
1184 if opts.git: 1238 if opts.git:
1185 copied = {} 1239 copied = {}
1186 for f in added: 1240 c1, c2 = ctx1, ctx2
1187 src = renamed(f) 1241 files = added
1242 man = man1
1243 if node2 and ctx1.rev() >= ctx2.rev():
1244 # renamed() starts at c2 and walks back in history until c1.
1245 # Since ctx1.rev() >= ctx2.rev(), invert ctx2 and ctx1 to
1246 # detect (inverted) copies.
1247 c1, c2 = ctx2, ctx1
1248 files = removed
1249 man = ctx2.manifest()
1250 for f in files:
1251 src = renamed(c1, c2, man, f)
1188 if src: 1252 if src:
1189 copied[f] = src 1253 copied[f] = src
1190 srcs = [x[1] for x in copied.items()] 1254 if ctx1 == c2:
1255 # invert the copied dict
1256 copied = dict([(v, k) for (k, v) in copied.iteritems()])
1257 # If we've renamed file foo to bar (copied['bar'] = 'foo'),
1258 # avoid showing a diff for foo if we're going to show
1259 # the rename to bar.
1260 srcs = [x[1] for x in copied.iteritems() if x[0] in added]
1191 1261
1192 all = modified + added + removed 1262 all = modified + added + removed
1193 all.sort() 1263 all.sort()
1194 gone = {} 1264 gone = {}
1195 1265
1200 header = [] 1270 header = []
1201 if f in man1: 1271 if f in man1:
1202 to = getfilectx(f, ctx1).data() 1272 to = getfilectx(f, ctx1).data()
1203 if f not in removed: 1273 if f not in removed:
1204 tn = getfilectx(f, ctx2).data() 1274 tn = getfilectx(f, ctx2).data()
1275 a, b = f, f
1205 if opts.git: 1276 if opts.git:
1206 def gitmode(x): 1277 def gitmode(x, l):
1207 return x and '100755' or '100644' 1278 return l and '120000' or (x and '100755' or '100644')
1208 def addmodehdr(header, omode, nmode): 1279 def addmodehdr(header, omode, nmode):
1209 if omode != nmode: 1280 if omode != nmode:
1210 header.append('old mode %s\n' % omode) 1281 header.append('old mode %s\n' % omode)
1211 header.append('new mode %s\n' % nmode) 1282 header.append('new mode %s\n' % nmode)
1212 1283
1213 a, b = f, f
1214 if f in added: 1284 if f in added:
1215 mode = gitmode(execf2(f)) 1285 mode = gitmode(execf2(f), linkf2(f))
1216 if f in copied: 1286 if f in copied:
1217 a = copied[f] 1287 a = copied[f]
1218 omode = gitmode(man1.execf(a)) 1288 omode = gitmode(man1.execf(a), man1.linkf(a))
1219 addmodehdr(header, omode, mode) 1289 addmodehdr(header, omode, mode)
1220 if a in removed and a not in gone: 1290 if a in removed and a not in gone:
1221 op = 'rename' 1291 op = 'rename'
1222 gone[a] = 1 1292 gone[a] = 1
1223 else: 1293 else:
1231 dodiff = 'binary' 1301 dodiff = 'binary'
1232 elif f in removed: 1302 elif f in removed:
1233 if f in srcs: 1303 if f in srcs:
1234 dodiff = False 1304 dodiff = False
1235 else: 1305 else:
1236 mode = gitmode(man1.execf(f)) 1306 mode = gitmode(man1.execf(f), man1.linkf(f))
1237 header.append('deleted file mode %s\n' % mode) 1307 header.append('deleted file mode %s\n' % mode)
1238 else: 1308 else:
1239 omode = gitmode(man1.execf(f)) 1309 omode = gitmode(man1.execf(f), man1.linkf(f))
1240 nmode = gitmode(execf2(f)) 1310 nmode = gitmode(execf2(f), linkf2(f))
1241 addmodehdr(header, omode, nmode) 1311 addmodehdr(header, omode, nmode)
1242 if util.binary(to) or util.binary(tn): 1312 if util.binary(to) or util.binary(tn):
1243 dodiff = 'binary' 1313 dodiff = 'binary'
1244 r = None 1314 r = None
1245 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b)) 1315 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1246 if dodiff: 1316 if dodiff:
1247 if dodiff == 'binary': 1317 if dodiff == 'binary':
1248 text = b85diff(fp, to, tn) 1318 text = b85diff(to, tn)
1249 else: 1319 else:
1250 text = mdiff.unidiff(to, date1, 1320 text = mdiff.unidiff(to, date1,
1251 # ctx2 date may be dynamic 1321 # ctx2 date may be dynamic
1252 tn, util.datestr(ctx2.date()), 1322 tn, util.datestr(ctx2.date()),
1253 f, r, opts=opts) 1323 a, b, r, opts=opts)
1254 if text or len(header) > 1: 1324 if text or len(header) > 1:
1255 fp.write(''.join(header)) 1325 fp.write(''.join(header))
1256 fp.write(text) 1326 fp.write(text)
1257 1327
1258 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False, 1328 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1301 return 1371 return
1302 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt") 1372 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1303 try: 1373 try:
1304 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name) 1374 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1305 try: 1375 try:
1306 for line in patchlines: print >> p.tochild, line 1376 for line in patchlines:
1377 p.tochild.write(line + "\n")
1307 p.tochild.close() 1378 p.tochild.close()
1308 if p.wait(): return 1379 if p.wait(): return
1309 fp = os.fdopen(fd, 'r') 1380 fp = os.fdopen(fd, 'r')
1310 stat = [] 1381 stat = []
1311 for line in fp: stat.append(line.lstrip()) 1382 for line in fp: stat.append(line.lstrip())
1312 last = stat.pop() 1383 last = stat.pop()
1313 stat.insert(0, last) 1384 stat.insert(0, last)
1314 stat = ''.join(stat) 1385 stat = ''.join(stat)
1315 if stat.startswith('0 files'): raise ValueError
1316 return stat 1386 return stat
1317 except: raise 1387 except: raise
1318 finally: 1388 finally:
1319 try: os.unlink(name) 1389 try: os.unlink(name)
1320 except: pass 1390 except: pass