--- a/mercurial/patch.py Thu Jul 26 07:56:27 2007 -0400
+++ b/mercurial/patch.py Wed Feb 06 19:57:52 2008 -0800
@@ -9,7 +9,7 @@
from i18n import _
from node import *
import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
-import cStringIO, email.Parser, os, popen2, re, sha
+import cStringIO, email.Parser, os, popen2, re, sha, errno
import sys, tempfile, zlib
class PatchError(Exception):
@@ -59,6 +59,7 @@
subject = msg['Subject']
user = msg['From']
+ gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
# should try to parse msg['Date']
date = None
nodeid = None
@@ -111,7 +112,7 @@
nodeid = line[10:]
elif line.startswith("# Parent "):
parents.append(line[10:])
- elif line == '---' and 'git-send-email' in msg['X-Mailer']:
+ elif line == '---' and gitsendmail:
ignoretext = True
if not line.startswith('# ') and not ignoretext:
cfp.write(line)
@@ -142,7 +143,7 @@
GP_FILTER = 1 << 1 # there's some copy/rename operation
GP_BINARY = 1 << 2 # there's a binary patch
-def readgitpatch(fp, firstline):
+def readgitpatch(fp, firstline=None):
"""extract git-style metadata about patches from <patchname>"""
class gitpatch:
"op is one of ADD, DELETE, RENAME, MODIFY or COPY"
@@ -151,12 +152,12 @@
self.oldpath = None
self.mode = None
self.op = 'MODIFY'
- self.copymod = False
self.lineno = 0
self.binary = False
def reader(fp, firstline):
- yield firstline
+ if firstline is not None:
+ yield firstline
for line in fp:
yield line
@@ -181,7 +182,6 @@
elif gp:
if line.startswith('--- '):
if gp.op in ('COPY', 'RENAME'):
- gp.copymod = True
dopatch |= GP_FILTER
gitpatches.append(gp)
gp = None
@@ -201,9 +201,9 @@
gp.op = 'DELETE'
elif line.startswith('new file mode '):
gp.op = 'ADD'
- gp.mode = int(line.rstrip()[-3:], 8)
+ gp.mode = int(line.rstrip()[-6:], 8)
elif line.startswith('new mode '):
- gp.mode = int(line.rstrip()[-3:], 8)
+ gp.mode = int(line.rstrip()[-6:], 8)
elif line.startswith('GIT binary patch'):
dopatch |= GP_BINARY
gp.binary = True
@@ -249,7 +249,7 @@
fuzz = False
if cwd:
args.append('-d %s' % util.shellquote(cwd))
- fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
+ fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
util.shellquote(patchname)))
for line in fp:
@@ -278,10 +278,13 @@
util.explain_exit(code)[0])
return fuzz
-def internalpatch(patchname, ui, strip, cwd, files):
- """use builtin patch to apply <patchname> to the working directory.
+def internalpatch(patchobj, ui, strip, cwd, files={}):
+ """use builtin patch to apply <patchobj> to the working directory.
returns whether patch was applied with fuzz factor."""
- fp = file(patchname, 'rb')
+ try:
+ fp = file(patchobj, 'rb')
+ except TypeError:
+ fp = patchobj
if cwd:
curdir = os.getcwd()
os.chdir(cwd)
@@ -299,25 +302,27 @@
contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
class patchfile:
- def __init__(self, ui, fname):
+ def __init__(self, ui, fname, missing=False):
self.fname = fname
self.ui = ui
- try:
- fp = file(fname, 'rb')
- self.lines = fp.readlines()
- self.exists = True
- except IOError:
+ self.lines = []
+ self.exists = False
+ self.missing = missing
+ if not missing:
+ try:
+ fp = file(fname, 'rb')
+ self.lines = fp.readlines()
+ self.exists = True
+ except IOError:
+ pass
+ else:
+ self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
+
+ if not self.exists:
dirname = os.path.dirname(fname)
if dirname and not os.path.isdir(dirname):
- dirs = dirname.split(os.path.sep)
- d = ""
- for x in dirs:
- d = os.path.join(d, x)
- if not os.path.isdir(d):
- os.mkdir(d)
- self.lines = []
- self.exists = False
-
+ os.makedirs(dirname)
+
self.hash = {}
self.dirty = 0
self.offset = 0
@@ -346,7 +351,7 @@
vala = abs(a - linenum)
valb = abs(b - linenum)
return cmp(vala, valb)
-
+
try:
cand = self.hash[l]
except:
@@ -354,7 +359,7 @@
if len(cand) > 1:
# resort our list of potentials forward then back.
- cand.sort(cmp=sorter)
+ cand.sort(sorter)
return cand
def hashlines(self):
@@ -399,11 +404,13 @@
st = None
try:
st = os.lstat(dest)
- if st.st_nlink > 1:
- os.unlink(dest)
- except: pass
+ except OSError, inst:
+ if inst.errno != errno.ENOENT:
+ raise
+ if st and st.st_nlink > 1:
+ os.unlink(dest)
fp = file(dest, 'wb')
- if st:
+ if st and st.st_nlink > 1:
os.chmod(dest, st.st_mode)
fp.writelines(self.lines)
fp.close()
@@ -422,6 +429,10 @@
if reverse:
h.reverse()
+ if self.missing:
+ self.rej.append(h)
+ return -1
+
if self.exists and h.createfile():
self.ui.warn(_("file %s already exists\n") % self.fname)
self.rej.append(h)
@@ -494,7 +505,7 @@
return -1
class hunk:
- def __init__(self, desc, num, lr, context):
+ def __init__(self, desc, num, lr, context, gitpatch=None):
self.number = num
self.desc = desc
self.hunk = [ desc ]
@@ -504,6 +515,7 @@
self.read_context_hunk(lr)
else:
self.read_unified_hunk(lr)
+ self.gitpatch = gitpatch
def read_unified_hunk(self, lr):
m = unidesc.match(self.desc)
@@ -658,10 +670,12 @@
return len(self.a) == self.lena and len(self.b) == self.lenb
def createfile(self):
- return self.starta == 0 and self.lena == 0
+ create = self.gitpatch is None or self.gitpatch.op == 'ADD'
+ return self.starta == 0 and self.lena == 0 and create
def rmfile(self):
- return self.startb == 0 and self.lenb == 0
+ remove = self.gitpatch is None or self.gitpatch.op == 'DELETE'
+ return self.startb == 0 and self.lenb == 0 and remove
def fuzzit(self, l, fuzz, toponly):
# this removes context lines from the top and bottom of list 'l'. It
@@ -702,7 +716,7 @@
def old(self, fuzz=0, toponly=False):
return self.fuzzit(self.a, fuzz, toponly)
-
+
def newctrl(self):
res = []
for x in self.hunk:
@@ -762,7 +776,7 @@
def parsefilename(str):
# --- filename \t|space stuff
- s = str[4:]
+ s = str[4:].rstrip('\r\n')
i = s.find('\t')
if i < 0:
i = s.find(' ')
@@ -791,31 +805,32 @@
nulla = afile_orig == "/dev/null"
nullb = bfile_orig == "/dev/null"
afile = pathstrip(afile_orig, strip)
- gooda = os.path.exists(afile) and not nulla
+ gooda = not nulla and os.path.exists(afile)
bfile = pathstrip(bfile_orig, strip)
if afile == bfile:
goodb = gooda
else:
- goodb = os.path.exists(bfile) and not nullb
+ goodb = not nullb and os.path.exists(bfile)
createfunc = hunk.createfile
if reverse:
createfunc = hunk.rmfile
- if not goodb and not gooda and not createfunc():
- raise PatchError(_("unable to find %s or %s for patching") %
- (afile, bfile))
- if gooda and goodb:
- fname = bfile
- if afile in bfile:
+ missing = not goodb and not gooda and not createfunc()
+ fname = None
+ if not missing:
+ if gooda and goodb:
+ fname = (afile in bfile) and afile or bfile
+ elif gooda:
fname = afile
- elif gooda:
- fname = afile
- elif not nullb:
- fname = bfile
- if afile in bfile:
+
+ if not fname:
+ if not nullb:
+ fname = (afile in bfile) and afile or bfile
+ elif not nulla:
fname = afile
- elif not nulla:
- fname = afile
- return fname
+ else:
+ raise PatchError(_("undefined source and destination files"))
+
+ return fname, missing
class linereader:
# simple class to allow pushing lines back into the input stream
@@ -833,14 +848,16 @@
return l
return self.fp.readline()
-def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
- rejmerge=None, updatedir=None):
- """reads a patch from fp and tries to apply it. The dict 'changed' is
- filled in with all of the filenames changed by the patch. Returns 0
- for a clean patch, -1 if any rejects were found and 1 if there was
- any fuzz."""
+def iterhunks(ui, fp, sourcefile=None):
+ """Read a patch and yield the following events:
+ - ("file", afile, bfile, firsthunk): select a new target file.
+ - ("hunk", hunk): a new hunk is ready to be applied, follows a
+ "file" event.
+ - ("git", gitchanges): current diff is in git format, gitchanges
+ maps filenames to gitpatch records. Unique event.
+ """
- def scangitpatch(fp, firstline, cwd=None):
+ def scangitpatch(fp, firstline):
'''git patches can modify a file, then copy that file to
a new file, but expect the source to be the unmodified form.
So we scan the patch looking for that case so we can do
@@ -853,28 +870,23 @@
fp = cStringIO.StringIO(fp.read())
(dopatch, gitpatches) = readgitpatch(fp, firstline)
- for gp in gitpatches:
- if gp.copymod:
- copyfile(gp.oldpath, gp.path, basedir=cwd)
-
fp.seek(pos)
return fp, dopatch, gitpatches
+ changed = {}
current_hunk = None
- current_file = None
afile = ""
bfile = ""
state = None
hunknum = 0
- rejects = 0
+ emitfile = False
git = False
gitre = re.compile('diff --git (a/.*) (b/.*)')
# our states
BFILE = 1
- err = 0
context = None
lr = linereader(fp)
dopatch = True
@@ -888,11 +900,7 @@
if current_hunk:
if x.startswith('\ '):
current_hunk.fix_newline()
- ret = current_file.apply(current_hunk, reverse)
- if ret >= 0:
- changed.setdefault(current_file.fname, (None, None))
- if ret > 0:
- err = 1
+ yield 'hunk', current_hunk
current_hunk = None
gitworkdone = False
if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
@@ -900,29 +908,22 @@
try:
if context == None and x.startswith('***************'):
context = True
- current_hunk = hunk(x, hunknum + 1, lr, context)
+ gpatch = changed.get(bfile[2:], (None, None))[1]
+ current_hunk = hunk(x, hunknum + 1, lr, context, gpatch)
except PatchError, err:
ui.debug(err)
current_hunk = None
continue
hunknum += 1
- if not current_file:
- if sourcefile:
- current_file = patchfile(ui, sourcefile)
- else:
- current_file = selectfile(afile, bfile, current_hunk,
- strip, reverse)
- current_file = patchfile(ui, current_file)
+ if emitfile:
+ emitfile = False
+ yield 'file', (afile, bfile, current_hunk)
elif state == BFILE and x.startswith('GIT binary patch'):
current_hunk = binhunk(changed[bfile[2:]][1])
- if not current_file:
- if sourcefile:
- current_file = patchfile(ui, sourcefile)
- else:
- current_file = selectfile(afile, bfile, current_hunk,
- strip, reverse)
- current_file = patchfile(ui, current_file)
hunknum += 1
+ if emitfile:
+ emitfile = False
+ yield 'file', (afile, bfile, current_hunk)
current_hunk.extract(fp)
elif x.startswith('diff --git'):
# check for git diff, scanning the whole patch file if needed
@@ -932,6 +933,7 @@
if not git:
git = True
fp, dopatch, gitpatches = scangitpatch(fp, x)
+ yield 'git', gitpatches
for gp in gitpatches:
changed[gp.path] = (gp.op, gp)
# else error?
@@ -968,36 +970,79 @@
bfile = parsefilename(l2)
if newfile:
- if current_file:
- current_file.close()
- if rejmerge:
- rejmerge(current_file)
- rejects += len(current_file.rej)
+ emitfile = True
state = BFILE
- current_file = None
hunknum = 0
if current_hunk:
if current_hunk.complete():
+ yield 'hunk', current_hunk
+ else:
+ raise PatchError(_("malformed patch %s %s") % (afile,
+ current_hunk.desc))
+
+ if hunknum == 0 and dopatch and not gitworkdone:
+ raise NoHunks
+
+def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
+ rejmerge=None, updatedir=None):
+ """reads a patch from fp and tries to apply it. The dict 'changed' is
+ filled in with all of the filenames changed by the patch. Returns 0
+ for a clean patch, -1 if any rejects were found and 1 if there was
+ any fuzz."""
+
+ rejects = 0
+ err = 0
+ current_file = None
+ gitpatches = None
+
+ def closefile():
+ if not current_file:
+ return 0
+ current_file.close()
+ if rejmerge:
+ rejmerge(current_file)
+ return len(current_file.rej)
+
+ for state, values in iterhunks(ui, fp, sourcefile):
+ if state == 'hunk':
+ if not current_file:
+ continue
+ current_hunk = values
ret = current_file.apply(current_hunk, reverse)
if ret >= 0:
changed.setdefault(current_file.fname, (None, None))
if ret > 0:
err = 1
+ elif state == 'file':
+ rejects += closefile()
+ afile, bfile, first_hunk = values
+ try:
+ if sourcefile:
+ current_file = patchfile(ui, sourcefile)
+ else:
+ current_file, missing = selectfile(afile, bfile, first_hunk,
+ strip, reverse)
+ current_file = patchfile(ui, current_file, missing)
+ except PatchError, err:
+ ui.warn(str(err) + '\n')
+ current_file, current_hunk = None, None
+ rejects += 1
+ continue
+ elif state == 'git':
+ gitpatches = values
+ for gp in gitpatches:
+ if gp.op in ('COPY', 'RENAME'):
+ copyfile(gp.oldpath, gp.path)
+ changed[gp.path] = (gp.op, gp)
else:
- fname = current_file and current_file.fname or None
- raise PatchError(_("malformed patch %s %s") % (fname,
- current_hunk.desc))
- if current_file:
- current_file.close()
- if rejmerge:
- rejmerge(current_file)
- rejects += len(current_file.rej)
- if updatedir and git:
+ raise util.Abort(_('unsupported parser state: %s') % state)
+
+ rejects += closefile()
+
+ if updatedir and gitpatches:
updatedir(gitpatches)
if rejects:
return -1
- if hunknum == 0 and dopatch and not gitworkdone:
- raise NoHunks
return err
def diffopts(ui, opts={}, untrusted=False):
@@ -1027,15 +1072,13 @@
for f in patches:
ctype, gp = patches[f]
if ctype == 'RENAME':
- copies.append((gp.oldpath, gp.path, gp.copymod))
+ copies.append((gp.oldpath, gp.path))
removes[gp.oldpath] = 1
elif ctype == 'COPY':
- copies.append((gp.oldpath, gp.path, gp.copymod))
+ copies.append((gp.oldpath, gp.path))
elif ctype == 'DELETE':
removes[gp.path] = 1
- for src, dst, after in copies:
- if not after:
- copyfile(src, dst, repo.root)
+ for src, dst in copies:
repo.copy(src, dst)
removes = removes.keys()
if removes:
@@ -1044,13 +1087,17 @@
for f in patches:
ctype, gp = patches[f]
if gp and gp.mode:
- x = gp.mode & 0100 != 0
+ flags = ''
+ if gp.mode & 0100:
+ flags = 'x'
+ elif gp.mode & 020000:
+ flags = 'l'
dst = os.path.join(repo.root, gp.path)
# patch won't create empty files
if ctype == 'ADD' and not os.path.exists(dst):
- repo.wwrite(gp.path, '', x and 'x' or '')
+ repo.wwrite(gp.path, '', flags)
else:
- util.set_exec(dst, x)
+ util.set_flags(dst, flags)
cmdutil.addremove(repo, cfiles)
files = patches.keys()
files.extend([r for r in removes if r not in files])
@@ -1058,7 +1105,7 @@
return files
-def b85diff(fp, to, tn):
+def b85diff(to, tn):
'''print base85-encoded binary diff'''
def gitindex(text):
if not text:
@@ -1142,24 +1189,30 @@
if node2:
ctx2 = context.changectx(repo, node2)
execf2 = ctx2.manifest().execf
+ linkf2 = ctx2.manifest().linkf
else:
ctx2 = context.workingctx(repo)
execf2 = util.execfunc(repo.root, None)
+ linkf2 = util.linkfunc(repo.root, None)
if execf2 is None:
- execf2 = ctx2.parents()[0].manifest().copy().execf
+ mc = ctx2.parents()[0].manifest().copy()
+ execf2 = mc.execf
+ linkf2 = mc.linkf
# returns False if there was no rename between ctx1 and ctx2
# returns None if the file was created between ctx1 and ctx2
# returns the (file, node) present in ctx1 that was renamed to f in ctx2
- def renamed(f):
- startrev = ctx1.rev()
- c = ctx2
+ # This will only really work if c1 is the Nth 1st parent of c2.
+ def renamed(c1, c2, man, f):
+ startrev = c1.rev()
+ c = c2
crev = c.rev()
if crev is None:
crev = repo.changelog.count()
orig = f
+ files = (f,)
while crev > startrev:
- if f in c.files():
+ if f in files:
try:
src = getfilectx(f, c).renamed()
except revlog.LookupError:
@@ -1169,7 +1222,8 @@
crev = c.parents()[0].rev()
# try to reuse
c = getctx(crev)
- if f not in man1:
+ files = c.files()
+ if f not in man:
return None
if f == orig:
return False
@@ -1183,11 +1237,27 @@
if opts.git:
copied = {}
- for f in added:
- src = renamed(f)
+ c1, c2 = ctx1, ctx2
+ files = added
+ man = man1
+ if node2 and ctx1.rev() >= ctx2.rev():
+ # renamed() starts at c2 and walks back in history until c1.
+ # Since ctx1.rev() >= ctx2.rev(), invert ctx2 and ctx1 to
+ # detect (inverted) copies.
+ c1, c2 = ctx2, ctx1
+ files = removed
+ man = ctx2.manifest()
+ for f in files:
+ src = renamed(c1, c2, man, f)
if src:
copied[f] = src
- srcs = [x[1] for x in copied.items()]
+ if ctx1 == c2:
+ # invert the copied dict
+ copied = dict([(v, k) for (k, v) in copied.iteritems()])
+ # If we've renamed file foo to bar (copied['bar'] = 'foo'),
+ # avoid showing a diff for foo if we're going to show
+ # the rename to bar.
+ srcs = [x[1] for x in copied.iteritems() if x[0] in added]
all = modified + added + removed
all.sort()
@@ -1202,20 +1272,20 @@
to = getfilectx(f, ctx1).data()
if f not in removed:
tn = getfilectx(f, ctx2).data()
+ a, b = f, f
if opts.git:
- def gitmode(x):
- return x and '100755' or '100644'
+ def gitmode(x, l):
+ return l and '120000' or (x and '100755' or '100644')
def addmodehdr(header, omode, nmode):
if omode != nmode:
header.append('old mode %s\n' % omode)
header.append('new mode %s\n' % nmode)
- a, b = f, f
if f in added:
- mode = gitmode(execf2(f))
+ mode = gitmode(execf2(f), linkf2(f))
if f in copied:
a = copied[f]
- omode = gitmode(man1.execf(a))
+ omode = gitmode(man1.execf(a), man1.linkf(a))
addmodehdr(header, omode, mode)
if a in removed and a not in gone:
op = 'rename'
@@ -1233,11 +1303,11 @@
if f in srcs:
dodiff = False
else:
- mode = gitmode(man1.execf(f))
+ mode = gitmode(man1.execf(f), man1.linkf(f))
header.append('deleted file mode %s\n' % mode)
else:
- omode = gitmode(man1.execf(f))
- nmode = gitmode(execf2(f))
+ omode = gitmode(man1.execf(f), man1.linkf(f))
+ nmode = gitmode(execf2(f), linkf2(f))
addmodehdr(header, omode, nmode)
if util.binary(to) or util.binary(tn):
dodiff = 'binary'
@@ -1245,12 +1315,12 @@
header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
if dodiff:
if dodiff == 'binary':
- text = b85diff(fp, to, tn)
+ text = b85diff(to, tn)
else:
text = mdiff.unidiff(to, date1,
# ctx2 date may be dynamic
tn, util.datestr(ctx2.date()),
- f, r, opts=opts)
+ a, b, r, opts=opts)
if text or len(header) > 1:
fp.write(''.join(header))
fp.write(text)
@@ -1303,7 +1373,8 @@
try:
p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
try:
- for line in patchlines: print >> p.tochild, line
+ for line in patchlines:
+ p.tochild.write(line + "\n")
p.tochild.close()
if p.wait(): return
fp = os.fdopen(fd, 'r')
@@ -1312,7 +1383,6 @@
last = stat.pop()
stat.insert(0, last)
stat = ''.join(stat)
- if stat.startswith('0 files'): raise ValueError
return stat
except: raise
finally: