[Freeipa-devel] Experimental patchwork server

John Dennis jdennis at redhat.com
Tue Oct 23 13:53:08 UTC 2012


On 10/23/2012 09:00 AM, Simo Sorce wrote:
> I strongly suggest you use git-send-email instead of thunderbird, it
> makes everything a lot faster, see the instructions I sent in my
> followup email.

I wrote a python script to manage my patch submissions a while ago which 
might be useful to folks, it's attached.

The basic idea is you keep a directory of your patch submissions. Inside 
the directory is also a file that has then next number to use for your 
submission. By default it runs git format-patch selecting the last 
commit. It creates a patch file using the patch submission format 
defined for IPA. If you use the -s option it also sends it to the list. 
It doesn't use git-send-email, rather it builds an email with a mime 
attachment according to our IPA recommendations. I don't recall why I 
didn't use git-send-email, but there was some reason (probably because I 
couldn't get it follow the IPA conventions, not sure though).

If you have to rework a patch use the -n option to specify which patch 
you're modifying. The script automatically searches the patch directory 
and finds the next revision number for the patch.

The config dict at the top will have to be modified to match your 
username, smtp server, etc. look for anything in UPPERCASE and replace 
with your specifics.

I like to use it because I don't have to remember my patch numbers and 
the result will always follow the IPA conventions without any fumbling 
around.

Petr3 will probably complain about using getopt and a config dict 
instead of optparse but it works and it wasn't worth it to me to port it 
to a different mechanism. Anybody which wants to is more than welcome.


-- 
John Dennis <jdennis at redhat.com>

Looking to carve out IT costs?
www.redhat.com/carveoutcosts/
-------------- next part --------------
#!/usr/bin/python

import getopt
import os
import errno
import sys
import subprocess
import re
import smtplib
import email
import traceback
from cStringIO import StringIO
from email.generator import Generator
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase

#-------------------------------------------------------------------------------

prog_name = os.path.basename(sys.argv[0])

config = {
    'project_name'          : 'freeipa',
    'user_name'             : 'USERNAME',
    'patch_dir'             : '/home/USERNAME/freeipa-patches',
    'smtp_server'           : 'SMTP_SERVER',
    'email_from_addr'       : 'FULLNAME <USERNAME at USER_DOMAIN>',
    'email_to_addrs'        : ['freeipa-devel at redhat.com'],
    'start_number_basename' : 'StartNumber',
    'default_number'        : 1,
    'number'                : None,
    'send'                  : False,
    'run_format'            : True,
    'dry_run'               : False,
    'verbose'               : False,
    'revision_range'        : '-1',
}

signature = '''
--
FULLNAME <USERNAME at USER_DOMAIN>
'''


#-------------------------------------------------------------------------------
git_filename_re = re.compile(r'^(\d+)-(.*).patch$')
ipa_filename_re = re.compile(r'^([^-]+)-([^-]+)-(\d+)(-(\d+))?-(.*)\.patch$')
patch_subject_re = re.compile(r'Subject:\s+\[PATCH\s+(\d+)(-(\d+))?\]')
#-------------------------------------------------------------------------------

class CommandError(Exception):
    def __init__(self, cmd, msg):
        self.cmd = cmd
        self.msg = msg

    def __str__(self):
        return "COMMAND ERROR: cmd='%s'\n%s" % (self.cmd, self.msg)

#-------------------------------------------------------------------------------

class PatchInfo:
    def __init__(self, project, user, number, revision, description, path=None):
        self.project = project
        self.user = user
        self.number = int(number)
        self.revision = int(revision)
        self.description = description
        self.path = path
        self.extension = 'patch'

    def __str__(self):
        extension = 'patch'

        if self.revision:
            filename = '%s-%s-%04d-%d-%s.%s' % \
                (self.project, self.user, self.number, self.revision, self.description, self.extension)
        else:
            filename = '%s-%s-%04d-%s.%s' % \
                (self.project, self.user, self.number, self.description, self.extension)

        return filename

    def __cmp__(self, other):
        result = cmp(self.project, other.project)
        if result != 0: return result

        result = cmp(self.user, other.user)
        if result != 0: return result

        result = cmp(self.number, other.number)
        if result != 0: return result

        result = cmp(self.revision, other.revision)
        if result != 0: return result

        result = cmp(self.description, other.description)
        if result != 0: return result

        return 0

#-------------------------------------------------------------------------------
def run_cmd(cmd):
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    status = os.waitpid(p.pid, 0)[1]
    msg = p.stdout.read().strip()
    if (status != 0):
        err_msg = p.stderr.read().strip()
        raise CommandError(cmd, err_msg)
    return msg

def next_number():
    try:
        f = open(config['start_number_filepath'], 'r')
        number = int(f.read())
        f.close()
        if config['verbose']:
            print "number %d read from '%s'" % (number, config['start_number_filepath'])
    except Exception, e:
        if e.errno == errno.ENOENT:
            number = config['default_number']
            if config['verbose']:
                print "'%s' does not exist yet, using default %d" % (config['start_number_filepath'], number)
        else:
            raise

    if not config['dry_run']:
        f = open(config['start_number_filepath'], 'w')
        f.write('%d\n' % (number + 1))
        f.close()

    return number

def get_patch_filename(number, revision, description):
    project = config['project_name']
    user = config['user_name']

    info = PatchInfo(project, user, number, revision, description)
    return str(info)

def find_patch(number, patch_dir):
    for filename in os.listdir(patch_dir):
        match = git_filename_re.search(filename)
        if match:
            number = int(match.group(1))
            description = match.group(2)
            if patch_number == number:
                patch_filename = filename
                return os.path.join(patch_dir, patch_filename)
    return None

def rename_git_patchfile(git_patchfile, number, revision):
    directory = os.path.dirname(git_patchfile)
    old_basename = os.path.basename(git_patchfile)
    match = git_filename_re.search(old_basename)
    if match:
        git_number = int(match.group(1))
        description = match.group(2)
        new_basename = get_patch_filename(number, revision, description)
        old_path = git_patchfile
        new_path = os.path.join(directory, new_basename)
        os.rename(old_path, new_path)
    else:
        raise ValueError("git_patchfile cannot be parsed (%s)" % (git_patchfile))
    return new_path

def parse_patch_filename(patch_filename):
    match = ipa_filename_re.search(patch_filename)
    if match is None:
        return None
    project = match.group(1)
    user = match.group(2)
    number = int(match.group(3))
    if match.group(4) is not None:
        revision = int(match.group(5))
    else:
        revision = 0
    description = match.group(6)
    return PatchInfo(project, user, number, revision, description, patch_filename)


def get_patch_revisions(number, patch_dir):
    patches = []
    for filename in os.listdir(patch_dir):
        info = parse_patch_filename(filename)
        if info is not None:
            if number == info.number:
                patches.append(info)

    if len(patches) == 0:
        return None

    patches.sort()
    revisions = [x.revision for x in patches]
    return revisions

def send_patch(filename):
    f = open(filename)
    patch = email.email.message_from_file(f)
    f.close()

    patch_name = os.path.basename(filename)

    # Get the entire raw message, including headers
    if False:
        f = StringIO()
        g = Generator(f, mangle_from_=False, maxheaderlen=0)
        g.flatten(patch)
        raw_msg = f.getvalue()
    else:
        f = open(filename)
        raw_msg = f.read()
        f.close()

    payload = patch.get_payload()
    i = payload.find('\n---\n')
    if i == -1:
        commit_msg = ''
    else:
        commit_msg = payload[:i]

    msg = MIMEMultipart()

    mime_part = MIMEText(commit_msg + '\n' + signature)
    msg.attach(mime_part)

    mime_part = MIMEBase('text', 'x-patch', name=patch_name)
    mime_part.set_charset('utf-8')
    mime_part.add_header('Content-Disposition', 'attachment', filename=patch_name)
    mime_part.set_payload(raw_msg)
    email.encoders.encode_base64(mime_part)
    msg.attach(mime_part)

    msg['Subject'] = patch['subject']
    msg['From']    = config['email_from_addr']
    msg['To']      = ', '.join(config['email_to_addrs'])

    if config['dry_run']:
        print msg
    else:
        s = smtplib.SMTP(config['smtp_server'])
        s.sendmail(config['email_from_addr'], config['email_to_addrs'], msg.as_string())
        s.quit()


def usage():
    '''
    Print command help.
    '''

    print '''\
-h --help               print help
-n --number nn          use this number for patch instead of next sequence number
-r --revision-range     use this revision range instead of default -1
-s --send               send patch using git send-email
-N --dry-run            don't execute, just report what would have been done
-F --no-format          don't run git format-patch

Examples:

%(prog_name)s
''' % {'prog_name' : prog_name,
      }

#-------------------------------------------------------------------------------

def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:], 'hn:r:sNF',
                                   ['help', 'number=', 'revision-range=', 'send', 'dry-run', 'no-format'])
    except getopt.GetoptError, err:
        print >>sys.stderr, str(err)
        usage()
        sys.exit(2)

    for o, a in opts:
        if o in ('-h', '--help'):
            usage()
            sys.exit()
        elif o in ('-n', '--number'):
            config['number'] = int(a)
        elif o in ('-r', '--revision-range'):
            config['revision_range'] = a
        elif o in ('-s', '--send'):
            config['send'] = True
        elif o in ('-N', '--dry-run'):
            config['dry_run'] = True
        elif o in ('-F', '--no-format'):
            config['run_format'] = False
        else:
            assert False, 'unhandled option'

    config['start_number_filepath'] = os.path.join(config['patch_dir'],
                                                   config['start_number_basename'])

    try:
        if config['dry_run']:
            patch_dir = '.'
        else:
            patch_dir = config['patch_dir']

        revision = 0
        if config['number'] is None:
            number = next_number()
        else:
            number = config['number']
            revisions = get_patch_revisions(number, patch_dir)
            if revisions is not None:
                revision = revisions[-1] + 1

        if config['run_format']:
            if len(args) > 0:
                extra_args = ' '.join(args)
            else:
                extra_args = config['revision_range']
            if revision:
                subject_number = '%d-%d' % (number, revision)
            else:
                subject_number = number
            cmd = 'git format-patch --start-number %d --subject-prefix "PATCH %s" -N -o %s' % \
                  (number, subject_number, patch_dir)
            cmd += ' ' + extra_args
            git_patchfile = run_cmd(cmd)
            patch_file = rename_git_patchfile(git_patchfile, number, revision)
            print patch_file

        if config['send']:
            send_patch(patch_file)

        return 0

    except Exception, e:
        print >>sys.stderr, e
        traceback.print_exc()
        return 1
#-------------------------------------------------------------------------------

if __name__ == '__main__':
    main()


More information about the Freeipa-devel mailing list