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 |
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/(.*)') |
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) |
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 |
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. |
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 |
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 |