##// END OF EJS Templates
hgext: more patchbomb documentation...
hgext: more patchbomb documentation + Add a description of how to enable this extension + Note which command it provides (it's not very easy to guess that "hgext.patchbomb" enables the "email" command, unless one can read Python sources) + Expand the descriptions of the -n and -m options of "hg email". + Mention that formail is (commonly) part of the procmail package.

File last commit:

r2922:773c5b82 merge default
r2926:13cd2cde default
Show More
patch.py
449 lines | 15.1 KiB | text/x-python | PythonLexer
Brendan Cully
Move patch-related code into its own module.
r2861 # patch.py - patch file parsing routines
#
Vadim Gelfer
merge git patch code.
r2865 # Copyright 2006 Brendan Cully <brendan@kublai.com>
#
Brendan Cully
Move patch-related code into its own module.
r2861 # This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.
from demandload import demandload
Vadim Gelfer
commands.import: refactor patch parsing into patch.extract.
r2866 from i18n import gettext as _
Vadim Gelfer
refactor text diff/patch code....
r2874 from node import *
demandload(globals(), "cmdutil mdiff util")
demandload(globals(), "cStringIO email.Parser os re shutil sys tempfile")
Vadim Gelfer
commands.import: refactor patch parsing into patch.extract.
r2866
def extract(ui, fileobj):
'''extract patch from data read from fileobj.
patch can be normal patch or contained in email message.
return tuple (filename, message, user, date). any item in returned
tuple can be None. if filename is None, fileobj did not contain
patch. caller must unlink filename when done.'''
# attempt to detect the start of a patch
# (this heuristic is borrowed from quilt)
diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
'retrieving revision [0-9]+(\.[0-9]+)*$|' +
'(---|\*\*\*)[ \t])', re.MULTILINE)
fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
tmpfp = os.fdopen(fd, 'w')
try:
hgpatch = False
msg = email.Parser.Parser().parse(fileobj)
message = msg['Subject']
user = msg['From']
# should try to parse msg['Date']
date = None
if message:
message = message.replace('\n\t', ' ')
ui.debug('Subject: %s\n' % message)
if user:
ui.debug('From: %s\n' % user)
diffs_seen = 0
ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
for part in msg.walk():
content_type = part.get_content_type()
ui.debug('Content-Type: %s\n' % content_type)
if content_type not in ok_types:
continue
payload = part.get_payload(decode=True)
m = diffre.search(payload)
if m:
ui.debug(_('found patch at byte %d\n') % m.start(0))
diffs_seen += 1
cfp = cStringIO.StringIO()
if message:
cfp.write(message)
cfp.write('\n')
for line in payload[:m.start(0)].splitlines():
if line.startswith('# HG changeset patch'):
ui.debug(_('patch generated by hg export\n'))
hgpatch = True
# drop earlier commit message content
cfp.seek(0)
cfp.truncate()
elif hgpatch:
if line.startswith('# User '):
user = line[7:]
ui.debug('From: %s\n' % user)
elif line.startswith("# Date "):
date = line[7:]
if not line.startswith('# '):
cfp.write(line)
cfp.write('\n')
message = cfp.getvalue()
if tmpfp:
tmpfp.write(payload)
if not payload.endswith('\n'):
tmpfp.write('\n')
elif not diffs_seen and message and content_type == 'text/plain':
message += '\n' + payload
except:
tmpfp.close()
os.unlink(tmpname)
raise
tmpfp.close()
if not diffs_seen:
os.unlink(tmpname)
return None, message, user, date
return tmpname, message, user, date
Brendan Cully
Move patch-related code into its own module.
r2861
def readgitpatch(patchname):
"""extract git-style metadata about patches from <patchname>"""
class gitpatch:
"op is one of ADD, DELETE, RENAME, MODIFY or COPY"
def __init__(self, path):
self.path = path
self.oldpath = None
self.mode = None
self.op = 'MODIFY'
self.copymod = False
self.lineno = 0
# Filter patch for git information
gitre = re.compile('diff --git a/(.*) b/(.*)')
pf = file(patchname)
gp = None
gitpatches = []
# Can have a git patch with only metadata, causing patch to complain
dopatch = False
lineno = 0
for line in pf:
lineno += 1
if line.startswith('diff --git'):
m = gitre.match(line)
if m:
if gp:
gitpatches.append(gp)
src, dst = m.group(1,2)
gp = gitpatch(dst)
gp.lineno = lineno
elif gp:
if line.startswith('--- '):
if gp.op in ('COPY', 'RENAME'):
gp.copymod = True
dopatch = 'filter'
gitpatches.append(gp)
gp = None
if not dopatch:
dopatch = True
continue
if line.startswith('rename from '):
gp.op = 'RENAME'
gp.oldpath = line[12:].rstrip()
elif line.startswith('rename to '):
gp.path = line[10:].rstrip()
elif line.startswith('copy from '):
gp.op = 'COPY'
gp.oldpath = line[10:].rstrip()
elif line.startswith('copy to '):
gp.path = line[8:].rstrip()
elif line.startswith('deleted file'):
gp.op = 'DELETE'
elif line.startswith('new file mode '):
gp.op = 'ADD'
gp.mode = int(line.rstrip()[-3:], 8)
elif line.startswith('new mode '):
gp.mode = int(line.rstrip()[-3:], 8)
if gp:
gitpatches.append(gp)
if not gitpatches:
dopatch = True
return (dopatch, gitpatches)
def dogitpatch(patchname, gitpatches):
"""Preprocess git patch so that vanilla patch can handle it"""
pf = file(patchname)
pfline = 1
fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
tmpfp = os.fdopen(fd, 'w')
try:
for i in range(len(gitpatches)):
p = gitpatches[i]
if not p.copymod:
continue
if os.path.exists(p.path):
raise util.Abort(_("cannot create %s: destination already exists") %
p.path)
(src, dst) = [os.path.join(os.getcwd(), n)
for n in (p.oldpath, p.path)]
targetdir = os.path.dirname(dst)
if not os.path.isdir(targetdir):
os.makedirs(targetdir)
try:
shutil.copyfile(src, dst)
shutil.copymode(src, dst)
except shutil.Error, inst:
raise util.Abort(str(inst))
# rewrite patch hunk
while pfline < p.lineno:
tmpfp.write(pf.readline())
pfline += 1
tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
line = pf.readline()
pfline += 1
while not line.startswith('--- a/'):
tmpfp.write(line)
line = pf.readline()
pfline += 1
tmpfp.write('--- a/%s\n' % p.path)
line = pf.readline()
while line:
tmpfp.write(line)
line = pf.readline()
except:
tmpfp.close()
os.unlink(patchname)
raise
tmpfp.close()
return patchname
Brendan Cully
Unify mq and hg patch invocation....
r2919 def patch(patchname, ui, strip=1, cwd=None):
Brendan Cully
Move patch-related code into its own module.
r2861 """apply the patch <patchname> to the working directory.
a list of patched files is returned"""
(dopatch, gitpatches) = readgitpatch(patchname)
files = {}
Brendan Cully
Unify mq and hg patch invocation....
r2919 fuzz = False
Brendan Cully
Move patch-related code into its own module.
r2861 if dopatch:
if dopatch == 'filter':
patchname = dogitpatch(patchname, gitpatches)
patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
args = []
if cwd:
args.append('-d %s' % util.shellquote(cwd))
fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
util.shellquote(patchname)))
if dopatch == 'filter':
False and os.unlink(patchname)
for line in fp:
line = line.rstrip()
Brendan Cully
Unify mq and hg patch invocation....
r2919 ui.note(line + '\n')
Brendan Cully
Move patch-related code into its own module.
r2861 if line.startswith('patching file '):
pf = util.parse_patch_output(line)
Brendan Cully
Unify mq and hg patch invocation....
r2919 printed_file = False
Brendan Cully
Move patch-related code into its own module.
r2861 files.setdefault(pf, (None, None))
Brendan Cully
Unify mq and hg patch invocation....
r2919 elif line.find('with fuzz') >= 0:
fuzz = True
if not printed_file:
ui.warn(pf + '\n')
printed_file = True
ui.warn(line + '\n')
elif line.find('saving rejects to file') >= 0:
ui.warn(line + '\n')
elif line.find('FAILED') >= 0:
if not printed_file:
ui.warn(pf + '\n')
printed_file = True
ui.warn(line + '\n')
Brendan Cully
Move patch-related code into its own module.
r2861 code = fp.close()
if code:
Vadim Gelfer
util: qualify name properly.
r2868 raise util.Abort(_("patch command failed: %s") %
util.explain_exit(code)[0])
Brendan Cully
Move patch-related code into its own module.
r2861
for gp in gitpatches:
files[gp.path] = (gp.op, gp)
Brendan Cully
Unify mq and hg patch invocation....
r2919 return (files, fuzz)
Vadim Gelfer
refactor text diff/patch code....
r2874
Matt Mackall
Move ui.diffopts to patch.diffopts where it belongs
r2888 def diffopts(ui, opts={}):
return mdiff.diffopts(
text=opts.get('text'),
Vadim Gelfer
redo merge with mpm....
r2921 git=(opts.get('git') or
ui.configbool('diff', 'git', None)),
Matt Mackall
Move ui.diffopts to patch.diffopts where it belongs
r2888 showfunc=(opts.get('show_function') or
ui.configbool('diff', 'showfunc', None)),
ignorews=(opts.get('ignore_all_space') or
ui.configbool('diff', 'ignorews', None)),
ignorewsamount=(opts.get('ignore_space_change') or
ui.configbool('diff', 'ignorewsamount', None)),
ignoreblanklines=(opts.get('ignore_blank_lines') or
ui.configbool('diff', 'ignoreblanklines', None)))
Vadim Gelfer
refactor text diff/patch code....
r2874 def diff(repo, node1=None, node2=None, files=None, match=util.always,
fp=None, changes=None, opts=None):
'''print diff of changes to files between two nodes, or node and
working directory.
if node1 is None, use first dirstate parent instead.
if node2 is None, compare node1 with working directory.'''
if opts is None:
opts = mdiff.defaultopts
if fp is None:
fp = repo.ui
if not node1:
node1 = repo.dirstate.parents()[0]
# reading the data for node1 early allows it to play nicely
Vadim Gelfer
remove localrepository.changes....
r2875 # with repo.status and the revlog cache.
Vadim Gelfer
refactor text diff/patch code....
r2874 change = repo.changelog.read(node1)
mmap = repo.manifest.read(change[0])
date1 = util.datestr(change[2])
if not changes:
Vadim Gelfer
remove localrepository.changes....
r2875 changes = repo.status(node1, node2, files, match=match)[:5]
Vadim Gelfer
refactor text diff/patch code....
r2874 modified, added, removed, deleted, unknown = changes
if files:
def filterfiles(filters):
Vadim Gelfer
fix patch.patch.filterfiles....
r2881 l = [x for x in filters if x in files]
Vadim Gelfer
refactor text diff/patch code....
r2874
Vadim Gelfer
fix patch.patch.filterfiles....
r2881 for t in files:
if not t.endswith("/"):
Vadim Gelfer
refactor text diff/patch code....
r2874 t += "/"
Vadim Gelfer
fix patch.patch.filterfiles....
r2881 l += [x for x in filters if x.startswith(t)]
Vadim Gelfer
refactor text diff/patch code....
r2874 return l
Vadim Gelfer
fix patch.patch.filterfiles....
r2881 modified, added, removed = map(filterfiles, (modified, added, removed))
Vadim Gelfer
refactor text diff/patch code....
r2874
if not modified and not added and not removed:
return
if node2:
change = repo.changelog.read(node2)
mmap2 = repo.manifest.read(change[0])
_date2 = util.datestr(change[2])
def date2(f):
return _date2
def read(f):
return repo.file(f).read(mmap2[f])
Brendan Cully
Add diff --git option
r2907 def renamed(f):
src = repo.file(f).renamed(mmap2[f])
return src and src[0] or None
Vadim Gelfer
refactor text diff/patch code....
r2874 else:
tz = util.makedate()[1]
_date2 = util.datestr()
def date2(f):
try:
return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
except OSError, err:
if err.errno != errno.ENOENT: raise
return _date2
def read(f):
return repo.wread(f)
Brendan Cully
Add diff --git option
r2907 def renamed(f):
return repo.dirstate.copies.get(f)
Vadim Gelfer
refactor text diff/patch code....
r2874
if repo.ui.quiet:
r = None
else:
hexfunc = repo.ui.verbose and hex or short
r = [hexfunc(node) for node in [node1, node2] if node]
Brendan Cully
Add diff --git option
r2907 if opts.git:
copied = {}
for f in added:
src = renamed(f)
if src:
copied[f] = src
srcs = [x[1] for x in copied.items()]
Vadim Gelfer
refactor text diff/patch code....
r2874 all = modified + added + removed
all.sort()
for f in all:
to = None
tn = None
Brendan Cully
Add diff --git option
r2907 dodiff = True
Vadim Gelfer
refactor text diff/patch code....
r2874 if f in mmap:
to = repo.file(f).read(mmap[f])
if f not in removed:
tn = read(f)
Brendan Cully
Add diff --git option
r2907 if opts.git:
def gitmode(x):
return 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
header = []
if f in added:
if node2:
mode = gitmode(mmap2.execf(f))
else:
mode = gitmode(util.is_exec(repo.wjoin(f), None))
if f in copied:
a = copied[f]
omode = gitmode(mmap.execf(a))
addmodehdr(header, omode, mode)
op = a in removed and 'rename' or 'copy'
header.append('%s from %s\n' % (op, a))
header.append('%s to %s\n' % (op, f))
to = repo.file(a).read(mmap[a])
else:
header.append('new file mode %s\n' % mode)
elif f in removed:
if f in srcs:
dodiff = False
else:
mode = gitmode(mmap.execf(f))
header.append('deleted file mode %s\n' % mode)
else:
omode = gitmode(mmap.execf(f))
nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
addmodehdr(header, omode, nmode)
r = None
if dodiff:
header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
fp.write(''.join(header))
if dodiff:
fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts))
Vadim Gelfer
refactor text diff/patch code....
r2874
def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
opts=None):
'''export changesets as hg patches.'''
total = len(revs)
revwidth = max(map(len, revs))
def single(node, seqno, fp):
parents = [p for p in repo.changelog.parents(node) if p != nullid]
if switch_parent:
parents.reverse()
prev = (parents and parents[0]) or nullid
change = repo.changelog.read(node)
if not fp:
fp = cmdutil.make_file(repo, template, node, total=total,
seqno=seqno, revwidth=revwidth)
if fp not in (sys.stdout, repo.ui):
repo.ui.note("%s\n" % fp.name)
fp.write("# HG changeset patch\n")
fp.write("# User %s\n" % change[1])
fp.write("# Date %d %d\n" % change[2])
fp.write("# Node ID %s\n" % hex(node))
fp.write("# Parent %s\n" % hex(prev))
if len(parents) > 1:
fp.write("# Parent %s\n" % hex(parents[1]))
fp.write(change[4].rstrip())
fp.write("\n\n")
diff(repo, prev, node, fp=fp, opts=opts)
if fp not in (sys.stdout, repo.ui):
fp.close()
for seqno, cset in enumerate(revs):
single(cset, seqno, fp)