clone
commit
cp
+ edit
export
files
float
unhide) _stg_patches $command _hidden_patches ;;
# patch commands
delete) _stg_patches $command _all_patches ;;
+ edit) _stg_patches $command _applied_patches ;;
export) _stg_patches $command _applied_patches ;;
files) _stg_patches $command _all_patches ;;
log) _stg_patches $command _all_patches ;;
# push the patches back
if not nopush:
push_patches(applied, merged)
+
+#
+# Patch description/e-mail/diff parsing
+#
+def __end_descr(line):
+ return re.match('---\s*$', line) or re.match('diff -', line) or \
+ re.match('Index: ', line)
+
+def __split_descr_diff(string):
+ """Return the description and the diff from the given string
+ """
+ descr = diff = ''
+ top = True
+
+ for line in string.split('\n'):
+ if top:
+ if not __end_descr(line):
+ descr += line + '\n'
+ continue
+ else:
+ top = False
+ diff += line + '\n'
+
+ return (descr.rstrip(), diff)
+
+def __parse_description(descr):
+ """Parse the patch description and return the new description and
+ author information (if any).
+ """
+ subject = body = ''
+ authname = authemail = authdate = None
+
+ descr_lines = [line.rstrip() for line in descr.split('\n')]
+ if not descr_lines:
+ raise CmdException, "Empty patch description"
+
+ lasthdr = 0
+ end = len(descr_lines)
+
+ # Parse the patch header
+ for pos in range(0, end):
+ if not descr_lines[pos]:
+ continue
+ # check for a "From|Author:" line
+ if re.match('\s*(?:from|author):\s+', descr_lines[pos], re.I):
+ auth = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
+ authname, authemail = name_email(auth)
+ lasthdr = pos + 1
+ continue
+ # check for a "Date:" line
+ if re.match('\s*date:\s+', descr_lines[pos], re.I):
+ authdate = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
+ lasthdr = pos + 1
+ continue
+ if subject:
+ break
+ # get the subject
+ subject = descr_lines[pos]
+ lasthdr = pos + 1
+
+ # get the body
+ if lasthdr < end:
+ body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
+
+ return (subject + body, authname, authemail, authdate)
+
+def parse_mail(msg):
+ """Parse the message object and return (description, authname,
+ authemail, authdate, diff)
+ """
+ from email.Header import decode_header, make_header
+
+ def __decode_header(header):
+ """Decode a qp-encoded e-mail header as per rfc2047"""
+ try:
+ words_enc = decode_header(header)
+ hobj = make_header(words_enc)
+ except Exception, ex:
+ raise CmdException, 'header decoding error: %s' % str(ex)
+ return unicode(hobj).encode('utf-8')
+
+ # parse the headers
+ if msg.has_key('from'):
+ authname, authemail = name_email(__decode_header(msg['from']))
+ else:
+ authname = authemail = None
+
+ # '\n\t' can be found on multi-line headers
+ descr = __decode_header(msg['subject']).replace('\n\t', ' ')
+ authdate = msg['date']
+
+ # remove the '[*PATCH*]' expression in the subject
+ if descr:
+ descr = re.findall('^(\[.*?[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
+ descr)[0][1]
+ else:
+ raise CmdException, 'Subject: line not found'
+
+ # the rest of the message
+ msg_text = ''
+ for part in msg.walk():
+ if part.get_content_type() == 'text/plain':
+ msg_text += part.get_payload(decode = True)
+
+ rem_descr, diff = __split_descr_diff(msg_text)
+ if rem_descr:
+ descr += '\n\n' + rem_descr
+
+ # parse the description for author information
+ descr, descr_authname, descr_authemail, descr_authdate = \
+ __parse_description(descr)
+ if descr_authname:
+ authname = descr_authname
+ if descr_authemail:
+ authemail = descr_authemail
+ if descr_authdate:
+ authdate = descr_authdate
+
+ return (descr, authname, authemail, authdate, diff)
+
+def parse_patch(fobj):
+ """Parse the input file and return (description, authname,
+ authemail, authdate, diff)
+ """
+ descr, diff = __split_descr_diff(fobj.read())
+ descr, authname, authemail, authdate = __parse_description(descr)
+
+ # we don't yet have an agreed place for the creation date.
+ # Just return None
+ return (descr, authname, authemail, authdate, diff)
--- /dev/null
+"""Patch editing command
+"""
+
+__copyright__ = """
+Copyright (C) 2007, Catalin Marinas <catalin.marinas@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+from optparse import OptionParser, make_option
+from email.Utils import formatdate
+
+from stgit.commands.common import *
+from stgit.utils import *
+from stgit.out import *
+from stgit import stack, git
+
+
+help = 'edit a patch description or diff'
+usage = """%prog [options] [<patch>]
+
+Edit the given patch (defaulting to the current one) description,
+author information or its diff (if the '--diff' option is
+passed). Without any other option, the command invokes the editor with
+the patch description and diff in the form below:
+
+ Subject line
+
+ From: author information
+ Date: creation date
+
+ Patch description
+
+ Signed-off-by: author
+
+Command-line options can be used to modify specific information
+without invoking the editor.
+
+If the patch diff is edited but the patch application fails, the
+rejected patch is stored in the .stgit-failed.patch file (and also in
+.stgit-edit.{diff,txt}). The edited patch can be replaced with one of
+these files using the '--file' and '--diff' options.
+"""
+
+options = [make_option('-d', '--diff',
+ help = 'allow the editing of the patch diff',
+ action = 'store_true'),
+ make_option('-f', '--file',
+ help = 'use FILE instead of invoking the editor'),
+ make_option('-O', '--diff-opts',
+ help = 'options to pass to git-diff'),
+ make_option('--undo',
+ help = 'revert the commit generated by the last edit',
+ action = 'store_true'),
+ make_option('-a', '--annotate', metavar = 'NOTE',
+ help = 'annotate the patch log entry'),
+ make_option('-m', '--message',
+ help = 'replace the patch description with MESSAGE'),
+ make_option('--author', metavar = '"NAME <EMAIL>"',
+ help = 'replae the author details with "NAME <EMAIL>"'),
+ make_option('--authname',
+ help = 'replace the author name with AUTHNAME'),
+ make_option('--authemail',
+ help = 'replace the author e-mail with AUTHEMAIL'),
+ make_option('--authdate',
+ help = 'replace the author date with AUTHDATE'),
+ make_option('--commname',
+ help = 'replace the committer name with COMMNAME'),
+ make_option('--commemail',
+ help = 'replace the committer e-mail with COMMEMAIL')
+ ] + make_sign_options()
+
+def __update_patch(pname, fname, options):
+ """Update the current patch from the given file.
+ """
+ patch = crt_series.get_patch(pname)
+
+ bottom = patch.get_bottom()
+ top = patch.get_top()
+
+ f = open(fname)
+ message, author_name, author_email, author_date, diff = parse_patch(f)
+ f.close()
+
+ if options.diff:
+ git.switch(bottom)
+ try:
+ git.apply_patch(fname)
+ except:
+ # avoid inconsistent repository state
+ git.switch(top)
+ raise
+
+ out.start('Updating patch "%s"' % pname)
+ crt_series.refresh_patch(message = message,
+ author_name = author_name,
+ author_email = author_email,
+ author_date = author_date,
+ backup = True, log = 'edit')
+ if crt_series.empty_patch(pname):
+ out.done('empty patch')
+ else:
+ out.done()
+
+def __edit_update_patch(pname, options):
+ """Edit the given patch interactively.
+ """
+ patch = crt_series.get_patch(pname)
+
+ if options.diff_opts:
+ if not options.diff:
+ raise CmdException, '--diff-opts only available with --diff'
+ diff_flags = options.diff_opts.split()
+ else:
+ diff_flags = []
+
+ # generate the file to be edited
+ descr = patch.get_description().strip()
+ descr_lines = descr.split('\n')
+ authdate = patch.get_authdate()
+
+ short_descr = descr_lines[0].rstrip()
+ long_descr = reduce(lambda x, y: x + '\n' + y,
+ descr_lines[1:], '').strip()
+
+ tmpl = '%(shortdescr)s\n\n' \
+ 'From: %(authname)s <%(authemail)s>\n'
+ if authdate:
+ tmpl += 'Date: %(authdate)s\n'
+ tmpl += '\n%(longdescr)s\n'
+
+ tmpl_dict = {
+ 'shortdescr': short_descr,
+ 'longdescr': long_descr,
+ 'authname': patch.get_authname(),
+ 'authemail': patch.get_authemail(),
+ 'authdate': patch.get_authdate()
+ }
+
+ if options.diff:
+ # add the patch diff to the edited file
+ bottom = patch.get_bottom()
+ top = patch.get_top()
+
+ tmpl += '---\n\n' \
+ '%(diffstat)s\n' \
+ '%(diff)s'
+
+ tmpl_dict['diffstat'] = git.diffstat(rev1 = bottom, rev2 = top)
+ tmpl_dict['diff'] = git.diff(rev1 = bottom, rev2 = top,
+ diff_flags = diff_flags)
+
+ for key in tmpl_dict:
+ # make empty strings if key is not available
+ if tmpl_dict[key] is None:
+ tmpl_dict[key] = ''
+
+ text = tmpl % tmpl_dict
+
+ if options.diff:
+ fname = '.stgit-edit.diff'
+ else:
+ fname = '.stgit-edit.txt'
+
+ # write the file to be edited
+ f = open(fname, 'w+')
+ f.write(text)
+ f.close()
+
+ # invoke the editor
+ call_editor(fname)
+
+ __update_patch(pname, fname, options)
+
+def func(parser, options, args):
+ """Edit the given patch or the current one.
+ """
+ crt_pname = crt_series.get_current()
+
+ if not args:
+ pname = crt_pname
+ if not pname:
+ raise CmdException, 'No patches applied'
+ elif len(args) == 1:
+ pname = args[0]
+ if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname):
+ raise CmdException, 'Cannot edit unapplied or hidden patches'
+ elif not crt_series.patch_applied(pname):
+ raise CmdException, 'Unknown patch "%s"' % pname
+ else:
+ parser.error('incorrect number of arguments')
+
+ check_local_changes()
+ check_conflicts()
+ check_head_top_equal()
+
+ if pname != crt_pname:
+ # Go to the patch to be edited
+ applied = crt_series.get_applied()
+ between = applied[:applied.index(pname):-1]
+ pop_patches(between)
+
+ if options.author:
+ options.authname, options.authemail = name_email(options.author)
+
+ if options.undo:
+ out.start('Undoing the editing of "%s"' % pname)
+ crt_series.undo_refresh()
+ out.done()
+ elif options.message or options.authname or options.authemail \
+ or options.authdate or options.commname or options.commemail \
+ or options.sign_str:
+ # just refresh the patch with the given information
+ out.start('Updating patch "%s"' % pname)
+ crt_series.refresh_patch(message = options.message,
+ author_name = options.authname,
+ author_email = options.authemail,
+ author_date = options.authdate,
+ committer_name = options.commname,
+ committer_email = options.commemail,
+ backup = True, sign_str = options.sign_str,
+ log = 'edit',
+ notes = options.annotate)
+ out.done()
+ elif options.file:
+ __update_patch(pname, options.file, options)
+ else:
+ __edit_update_patch(pname, options)
+
+ if pname != crt_pname:
+ # Push the patches back
+ between.reverse()
+ push_patches(between)
"""
import sys, os, re, email
-from email.Header import decode_header, make_header
from mailbox import UnixMailbox
from StringIO import StringIO
from optparse import OptionParser, make_option
] + make_sign_options()
-def __end_descr(line):
- return re.match('---\s*$', line) or re.match('diff -', line) or \
- re.match('Index: ', line)
-
def __strip_patch_name(name):
stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
return stripped
-def __split_descr_diff(string):
- """Return the description and the diff from the given string
- """
- descr = diff = ''
- top = True
-
- for line in string.split('\n'):
- if top:
- if not __end_descr(line):
- descr += line + '\n'
- continue
- else:
- top = False
- diff += line + '\n'
-
- return (descr.rstrip(), diff)
-
-def __parse_description(descr):
- """Parse the patch description and return the new description and
- author information (if any).
- """
- subject = body = ''
- authname = authemail = authdate = None
-
- descr_lines = [line.rstrip() for line in descr.split('\n')]
- if not descr_lines:
- raise CmdException, "Empty patch description"
-
- lasthdr = 0
- end = len(descr_lines)
-
- # Parse the patch header
- for pos in range(0, end):
- if not descr_lines[pos]:
- continue
- # check for a "From|Author:" line
- if re.match('\s*(?:from|author):\s+', descr_lines[pos], re.I):
- auth = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
- authname, authemail = name_email(auth)
- lasthdr = pos + 1
- continue
- # check for a "Date:" line
- if re.match('\s*date:\s+', descr_lines[pos], re.I):
- authdate = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
- lasthdr = pos + 1
- continue
- if subject:
- break
- # get the subject
- subject = descr_lines[pos]
- lasthdr = pos + 1
-
- # get the body
- if lasthdr < end:
- body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
-
- return (subject + body, authname, authemail, authdate)
-
-def __parse_mail(msg):
- """Parse the message object and return (description, authname,
- authemail, authdate, diff)
- """
- def __decode_header(header):
- """Decode a qp-encoded e-mail header as per rfc2047"""
- try:
- words_enc = decode_header(header)
- hobj = make_header(words_enc)
- except Exception, ex:
- raise CmdException, 'header decoding error: %s' % str(ex)
- return unicode(hobj).encode('utf-8')
-
- # parse the headers
- if msg.has_key('from'):
- authname, authemail = name_email(__decode_header(msg['from']))
- else:
- authname = authemail = None
-
- # '\n\t' can be found on multi-line headers
- descr = __decode_header(msg['subject']).replace('\n\t', ' ')
- authdate = msg['date']
-
- # remove the '[*PATCH*]' expression in the subject
- if descr:
- descr = re.findall('^(\[.*?[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
- descr)[0][1]
- else:
- raise CmdException, 'Subject: line not found'
-
- # the rest of the message
- msg_text = ''
- for part in msg.walk():
- if part.get_content_type() == 'text/plain':
- msg_text += part.get_payload(decode = True)
-
- rem_descr, diff = __split_descr_diff(msg_text)
- if rem_descr:
- descr += '\n\n' + rem_descr
-
- # parse the description for author information
- descr, descr_authname, descr_authemail, descr_authdate = \
- __parse_description(descr)
- if descr_authname:
- authname = descr_authname
- if descr_authemail:
- authemail = descr_authemail
- if descr_authdate:
- authdate = descr_authdate
-
- return (descr, authname, authemail, authdate, diff)
-
-def __parse_patch(fobj):
- """Parse the input file and return (description, authname,
- authemail, authdate, diff)
- """
- descr, diff = __split_descr_diff(fobj.read())
- descr, authname, authemail, authdate = __parse_description(descr)
-
- # we don't yet have an agreed place for the creation date.
- # Just return None
- return (descr, authname, authemail, authdate, diff)
-
def __create_patch(filename, message, author_name, author_email,
author_date, diff, options):
"""Create a new patch on the stack
except Exception, ex:
raise CmdException, 'error parsing the e-mail file: %s' % str(ex)
message, author_name, author_email, author_date, diff = \
- __parse_mail(msg)
+ parse_mail(msg)
else:
message, author_name, author_email, author_date, diff = \
- __parse_patch(f)
+ parse_patch(f)
if filename:
f.close()
for msg in mbox:
message, author_name, author_email, author_date, diff = \
- __parse_mail(msg)
+ parse_mail(msg)
__create_patch(None, message, author_name, author_email,
author_date, diff, options)
help = 'force the refresh even if HEAD and '\
'top differ',
action = 'store_true'),
- make_option('-e', '--edit',
- help = 'invoke an editor for the patch '\
- 'description',
- action = 'store_true'),
- make_option('-s', '--showpatch',
- help = 'show the patch content in the editor buffer',
- action = 'store_true'),
make_option('--update',
help = 'only update the current patch files',
action = 'store_true'),
make_option('--undo',
help = 'revert the commit generated by the last refresh',
action = 'store_true'),
- make_option('-m', '--message',
- help = 'use MESSAGE as the patch ' \
- 'description'),
make_option('-a', '--annotate', metavar = 'NOTE',
help = 'annotate the patch log entry'),
- make_option('--author', metavar = '"NAME <EMAIL>"',
- help = 'use "NAME <EMAIL>" as the author details'),
- make_option('--authname',
- help = 'use AUTHNAME as the author name'),
- make_option('--authemail',
- help = 'use AUTHEMAIL as the author e-mail'),
- make_option('--authdate',
- help = 'use AUTHDATE as the author date'),
- make_option('--commname',
- help = 'use COMMNAME as the committer name'),
- make_option('--commemail',
- help = 'use COMMEMAIL as the committer ' \
- 'e-mail'),
make_option('-p', '--patch',
help = 'refresh (applied) PATCH instead of the top one')
- ] + make_sign_options()
+ ]
def func(parser, options, args):
autoresolved = config.get('stgit.autoresolved')
out.done()
return
- if options.author:
- options.authname, options.authemail = name_email(options.author)
-
files = [path for (stat,path) in git.tree_status(verbose = True)]
if args:
files = [f for f in files if f in args]
- if files or not crt_series.head_top_equal() \
- or options.edit or options.message \
- or options.authname or options.authemail or options.authdate \
- or options.commname or options.commemail or options.sign_str:
-
+ if files or not crt_series.head_top_equal():
if options.patch:
applied = crt_series.get_applied()
between = applied[:applied.index(patch):-1]
if autoresolved == 'yes':
resolved_all()
crt_series.refresh_patch(files = files,
- message = options.message,
- edit = options.edit,
- show_patch = options.showpatch,
- author_name = options.authname,
- author_email = options.authemail,
- author_date = options.authdate,
- committer_name = options.commname,
- committer_email = options.commemail,
- backup = True, sign_str = options.sign_str,
- notes = options.annotate)
+ backup = True, notes = options.annotate)
if crt_series.empty_patch(patch):
out.done('empty patch')
else:
return ''
+# TODO: take another parameter representing a diff string as we
+# usually invoke git.diff() form the calling functions
def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
"""Return the diffstat between rev1 and rev2."""
return GRun('git-apply', '--stat', '--summary'
'clone': 'clone',
'commit': 'commit',
'cp': 'copy',
+ 'edit': 'edit',
'export': 'export',
'files': 'files',
'float': 'float',
"""
import sys, os, re
+from email.Utils import formatdate
from stgit.utils import *
from stgit.out import *
-from stgit.run import *
from stgit import git, basedir, templates
from stgit.config import config
from shutil import copyfile
f.seek(0); f.truncate()
f.writelines(lines)
+# TODO: move this out of the stgit.stack module, it is really for
+# higher level commands to handle the user interaction
def edit_file(series, line, comment, show_patch = True):
fname = '.stgitmsg.txt'
tmpl = templates.get_template('patchdescr.tmpl')
self._set_field('authemail', email or git.author().email)
def get_authdate(self):
- return self._get_field('authdate')
+ date = self._get_field('authdate')
+ if not date:
+ return date
+
+ if re.match('[0-9]+\s+[+-][0-9]+', date):
+ # Unix time (seconds) + time zone
+ secs_tz = date.split()
+ date = formatdate(int(secs_tz[0]))[:-5] + secs_tz[1]
+
+ return date
def set_authdate(self, date):
self._set_field('authdate', date or git.author().date)
elif message:
descr = message
+ # TODO: move this out of the stgit.stack module, it is really
+ # for higher level commands to handle the user interaction
if not message and edit:
descr = edit_file(self, descr.rstrip(), \
'Please edit the description for patch "%s" ' \
if self.patch_exists(name):
raise StackException, 'Patch "%s" already exists' % name
+ # TODO: move this out of the stgit.stack module, it is really
+ # for higher level commands to handle the user interaction
if not message and can_edit:
descr = edit_file(
self, None,
'Check the "push(f)" log' \
'
stg pop &&
- stg refresh -m "Foo2 Patch" &&
+ stg edit -m "Foo2 Patch" &&
stg push &&
stg log --full | grep -q -e "^push(f) "
'