[U-Boot] [RFC PATCH v2] Add patch submission script all in one commit

Anton Staaf robotboy at chromium.org
Fri Oct 21 01:13:47 CEST 2011


On Thu, Oct 20, 2011 at 3:39 PM, Simon Glass <sjg at chromium.org> wrote:
> This is a script for automating submission of patches to the U-Boot mailing
> list.
>
> Signed-off-by: Simon Glass <sjg at chromium.org>
> ---
> Changes in v2:
> - Fix up use of Color in series.py (avoid runtime error)
>
>  tools/scripts/patman/.gitignore     |    1 +
>  tools/scripts/patman/README         |  202 +++++++++++++++++
>  tools/scripts/patman/checkpatch.py  |  154 +++++++++++++
>  tools/scripts/patman/command.py     |   72 ++++++
>  tools/scripts/patman/commit.py      |   77 +++++++
>  tools/scripts/patman/gitutil.py     |  212 +++++++++++++++++
>  tools/scripts/patman/patchstream.py |  426 +++++++++++++++++++++++++++++++++++
>  tools/scripts/patman/patman         |    1 +
>  tools/scripts/patman/patman.py      |  127 +++++++++++
>  tools/scripts/patman/series.py      |  232 +++++++++++++++++++
>  tools/scripts/patman/settings.py    |   50 ++++
>  tools/scripts/patman/terminal.py    |   86 +++++++
>  tools/scripts/patman/test.py        |  248 ++++++++++++++++++++
>  13 files changed, 1888 insertions(+), 0 deletions(-)
>  create mode 100644 tools/scripts/patman/.gitignore
>  create mode 100644 tools/scripts/patman/README
>  create mode 100644 tools/scripts/patman/checkpatch.py
>  create mode 100644 tools/scripts/patman/command.py
>  create mode 100644 tools/scripts/patman/commit.py
>  create mode 100644 tools/scripts/patman/gitutil.py
>  create mode 100644 tools/scripts/patman/patchstream.py
>  create mode 120000 tools/scripts/patman/patman
>  create mode 100755 tools/scripts/patman/patman.py
>  create mode 100644 tools/scripts/patman/series.py
>  create mode 100644 tools/scripts/patman/settings.py
>  create mode 100644 tools/scripts/patman/terminal.py
>  create mode 100644 tools/scripts/patman/test.py
>
> diff --git a/tools/scripts/patman/.gitignore b/tools/scripts/patman/.gitignore
> new file mode 100644
> index 0000000..0d20b64
> --- /dev/null
> +++ b/tools/scripts/patman/.gitignore
> @@ -0,0 +1 @@
> +*.pyc
> diff --git a/tools/scripts/patman/README b/tools/scripts/patman/README
> new file mode 100644
> index 0000000..88655eb
> --- /dev/null
> +++ b/tools/scripts/patman/README
> @@ -0,0 +1,202 @@
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +What is this?
> +=============
> +
> +This tool is a python script which:
> +- Creates patch directly from your branch
> +- Cleans them up
> +- Inserts a cover letter and change lists
> +- Sends them out to selected people
> +
> +It is intended to automate patch creation and make it a less
> +error-prone process. It is useful for U-Boot and Linux work so far,
> +since it uses the kernel's checkpatch.pl script.
> +
> +It is configured almost entirely by tags it finds in your commits. So
> +for example if you put:
> +
> +Series-to: fred.blogs at napier.co.nz
> +
> +in one your commits, the series will be sent there (only one 'to' is
> +allowed at present).
> +
> +
> +How to use this tool
> +====================
> +
> +This tool requires a certain way of working:
> +
> +- Maintain a number of branches, one for each patch series you are
> +working on
> +- Add tags into the commits within each branch to indicate where the
> +series should be sent, cover letter, version, etc. Most of these are
> +normally in the top commit so it is easy to change them with 'git
> +commit --amend'
> +- Each branch tracks the upstream branch, so that this script can
> +automatically determine the number of commits in it
> +- Check out a branch, and run this script to create and send out your
> +patches
> +
> +
> +How to configure it
> +===================
> +
> +Create a file ~/.config/patman directory like this:
> +
> +>>>>
> +# Clean-patch alias file
> +
> +[alias]
> +me: Simon Glass <sjg at chromium.org>
> +
> +u-boot: U-Boot Mailing List <u-boot at lists.denx.de>
> +mikef: Mike Frysinger <vapier at gentoo.org>
> +wolfgang: Wolfgang Denk <wd at denx.de>
> +albert: Albert ARIBAUD <albert.u.boot at aribaud.net>
> +<<<<
> +
> +This contains useful aliases for people you want to send patches to
> +you. Note: This should probably use git's alias feature instead.
> +
> +Find checkpatch.pl from a Linux kernel tree, and put it in
> +~/bin/checkpatch.pl
> +
> +
> +How to run it
> +=============
> +
> +First do a dry run:
> +
> +$ ./tools/scripts/patman/patman -n
> +
> +If it can't detect the upstream branch, try telling it how many patches
> +there are in your series:
> +
> +$ ./tools/scripts/patman/patman -n -c5
> +
> +This will create patch files in your current directory and tell you who
> +it is thinking of sending them to. Take a look at the patch files.
> +
> +
> +How to add tags
> +===============
> +
> +To make this script useful you must add tags like the following. Most
> +can only appear once.
> +
> +Series-to: email / alias
> +        Email address / alias to send patch series to
> +
> +Series-cc: email / alias, ...
> +        Email address / alias to Cc patch series to (you can add this
> +        multiple times)
> +
> +Series-version: n
> +        Sets the version number of this patch series
> +
> +Series-prefix: prefix
> +        Sets the subject prefix. Normally empty but it can be RFC for
> +        RFC patches, or RESEND if you are being ignored.
> +
> +Cover-letter:
> +This is the patch set title
> +blah blah
> +more blah blah
> +END
> +        Sets the cover letter contents for the series. The first line
> +        will become the subject of the cover letter
> +
> +Series-notes:
> +blah blah
> +blah blah
> +more blah blah
> +END
> +        Sets some notes for the patch series, which you don't want in
> +        the commit messages, but do want to send, The notes are joined
> +        together and put after the cover letter. Can appear multiple
> +        times.
> +
> +Signed-off-by: Their Name <email>
> +        A sign-off is added automatically to your patches (this is
> +        probably a bug). If you put this tag in your patches, it will
> +        override your default signoff.
> +
> +Tested-by: Their Name <email>
> +Acked-by: Their Name <email>
> +        These indicate that someone has acked or tested your patch.
> +        When you get this reply on the mailing list, you can add this
> +        tag to the relevant commit and the script will include it when
> +        you send out the next version. If 'Tested-by:' is set to
> +        yourself, it will be removed. No one will believe you.
> +
> +Series-changes: n
> +- Guinea pig moved into its cage
> +- Other changes ending with a blank line
> +<blank line>
> +        This can appear in any commit. It lists the changes for a
> +        particular version n of that commit. The change list is
> +        created based on this information. Each commit gets its own
> +        change list and also the whole thing is repeated in the cover
> +        letter.
> +
> +        By adding your change lists into your commits it is easier to
> +        keep track of what happened. When you amend a commit, remember
> +        to update the log there and then, knowing that the script will
> +        do the rest.
> +
> +Various other tags are silently removed, like these Chrome OS and
> +Gerrit tags:
> +
> +BUG=...
> +TEST=...
> +Change-Id:
> +Review URL:
> +Reviewed-on:
> +Reviewed-by:
> +
> +
> +Exercise for the reader: Try adding some tags to one of your current
> +patch series and see how the patches turn out.
> +
> +
> +Other thoughts
> +==============
> +
> +This script has been split into sensible files but still needs work.
> +Most of these are indicated by a TODO in the code.
> +
> +It would be nice if this could handle the In-reply-to side of things.
> +
> +The git Cc: tag should be respected.
> +
> +The tests are incomplete. Use the -t flag to run them.
> +
> +Error handling doesn't always produce friendly error messages - e.g. putting an incorrect tag in a commit.
> +
> +There might be a few other features not mentioned in this README. They
> +might be bugs.
> +
> +
> +Simon Glass
> +sjg at chromium.org
> +19-Oct-11
> diff --git a/tools/scripts/patman/checkpatch.py b/tools/scripts/patman/checkpatch.py
> new file mode 100644
> index 0000000..5320cd5
> --- /dev/null
> +++ b/tools/scripts/patman/checkpatch.py
> @@ -0,0 +1,154 @@
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +import command
> +import os
> +import re
> +import terminal
> +
> +def FindCheckPatch():
> +    # Look in current dir
> +    for path in [os.getcwd(), '%s/bin' % os.getenv('HOME')]:
> +        fname = os.path.join(path, 'checkpatch.pl')
> +        if os.path.isfile(fname):
> +            return fname
> +
> +    # Look upwwards for a Chrome OS tree
> +    while not os.path.ismount(path):
> +        fname = os.path.join(path, 'src', 'third_party', 'kernel', 'files',
> +                'scripts', 'checkpatch.pl')
> +        if os.path.isfile(fname):
> +            return fname
> +        path = os.path.dirname(path)
> +    print 'Could not find checkpatch.pl'
> +    return None
> +

This should be able to be simplified now that checkpatch is being imported into
U-Boot.

> +def CheckPatch(fname, verbose=False):
> +    """Run checkpatch.pl on a file.
> +
> +    Returns:
> +        4-tuple containing:
> +            result: False=failure, True=ok
> +            problems: List of problems, each a dict:
> +                'type'; error or warning
> +                'msg': text message
> +                'file' : filename
> +                'line': line number
> +            lines: Number of lines
> +    """
> +    result = False
> +    error_count, warning_count, lines = 0, 0, 0
> +    problems = []
> +    chk = FindCheckPatch()
> +    if not chk:
> +        raise OSError, ('Cannot find checkpatch.pl - please put it in your ' +
> +                '~/bin directory')
> +    item = {}
> +    stdout = command.Output(chk, '--no-tree', fname)
> +    #pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
> +    #stdout, stderr = pipe.communicate()
> +
> +    # total: 0 errors, 0 warnings, 159 lines checked
> +    re_stats = re.compile('total: (\\d+) errors, (\d+) warnings, (\d+)')
> +    re_ok = re.compile('.*has no obvious style problems')
> +    re_bad = re.compile('.*has style problems, please review')
> +    re_error = re.compile('ERROR: (.*)')
> +    re_warning = re.compile('WARNING: (.*)')
> +    re_file = re.compile('#\d+: FILE: ([^:]*):(\d+):')
> +
> +    for line in stdout.splitlines():
> +        if verbose:
> +            print line
> +
> +        # A blank line indicates the end of a message
> +        if not line and item:
> +            problems.append(item)
> +            item = {}
> +        match = re_stats.match(line)
> +        if match:
> +            error_count = int(match.group(1))
> +            warning_count = int(match.group(2))
> +            lines = int(match.group(3))
> +        elif re_ok.match(line):
> +            result = True
> +        elif re_bad.match(line):
> +            result = False
> +        match = re_error.match(line)
> +        if match:
> +            item['msg'] = match.group(1)
> +            item['type'] = 'error'
> +        match = re_warning.match(line)
> +        if match:
> +            item['msg'] = match.group(1)
> +            item['type'] = 'warning'
> +        match = re_file.match(line)
> +        if match:
> +            item['file'] = match.group(1)
> +            item['line'] = int(match.group(2))
> +
> +    return result, problems, error_count, warning_count, lines, stdout
> +
> +def GetWarningMsg(col, msg_type, fname, line, msg):
> +    '''Create a message for a given file/line
> +
> +    Args:
> +        msg_type: Message type ('error' or 'warning')
> +        fname: Filename which reports the problem
> +        line: Line number where it was noticed
> +        msg: Message to report
> +    '''
> +    if msg_type == 'warning':
> +        msg_type = col.Color(col.YELLOW, msg_type)
> +    elif msg_type == 'error':
> +        msg_type = col.Color(col.RED, msg_type)
> +    return '%s: %s,%d: %s' % (msg_type, fname, line, msg)
> +
> +def CheckPatches(verbose, args):
> +    '''Run the checkpatch.pl script on each patch'''
> +    error_count = 0
> +    warning_count = 0
> +    col = terminal.Color()
> +
> +    for fname in args:
> +        ok, problems, errors, warnings, lines, stdout = CheckPatch(fname,
> +                verbose)
> +        if not ok:
> +            error_count += errors
> +            warning_count += warnings
> +            print '%d errors, %d warnings for %s:' % (errors,
> +                    warnings, fname)
> +            if len(problems) != error_count + warning_count:
> +                print "Internal error: some problems lost"
> +            for item in problems:
> +                print GetWarningMsg(col, item['type'], item['file'],
> +                        item['line'], item['msg'])
> +            #print stdout
> +    if error_count != 0 or warning_count != 0:
> +        str = 'checkpatch.pl found %d error(s), %d warning(s)' % (
> +            error_count, warning_count)
> +        color = col.GREEN
> +        if warning_count:
> +            color = col.YELLOW
> +        if error_count:
> +            color = col.RED
> +        print col.Color(color, str)
> +        return False
> +    return True
> diff --git a/tools/scripts/patman/command.py b/tools/scripts/patman/command.py
> new file mode 100644
> index 0000000..4b00250
> --- /dev/null
> +++ b/tools/scripts/patman/command.py
> @@ -0,0 +1,72 @@
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +import os
> +import subprocess
> +
> +"""Shell command ease-ups for Python."""
> +
> +def RunPipe(pipeline, infile=None, outfile=None,
> +            capture=False, oneline=False, hide_stderr=False):
> +    """
> +    Perform a command pipeline, with optional input/output filenames.
> +
> +    hide_stderr     Don't allow output of stderr (default False)
> +    """
> +    last_pipe = None
> +    while pipeline:
> +        cmd = pipeline.pop(0)
> +        kwargs = {}
> +        if last_pipe is not None:
> +            kwargs['stdin'] = last_pipe.stdout
> +        elif infile:
> +            kwargs['stdin'] = open(infile, 'rb')
> +        if pipeline or capture:
> +            kwargs['stdout'] = subprocess.PIPE
> +        elif outfile:
> +            kwargs['stdout'] = open(outfile, 'wb')
> +        if hide_stderr:
> +            kwargs['stderr'] = open('/dev/null', 'wb')
> +
> +        last_pipe = subprocess.Popen(cmd, **kwargs)
> +
> +    if capture:
> +        ret = last_pipe.communicate()[0]
> +        if not ret:
> +            return None
> +        elif oneline:
> +            return ret.rstrip('\r\n')
> +        else:
> +            return ret
> +    else:
> +        return os.waitpid(last_pipe.pid, 0)[1] == 0
> +
> +def Output(*cmd):
> +    return RunPipe([cmd], capture=True)
> +
> +def OutputOneLine(*cmd):
> +    return RunPipe([cmd], capture=True, oneline=True)
> +
> +def Run(*cmd, **kwargs):
> +    return RunPipe([cmd], **kwargs)
> +
> +def RunList(cmd):
> +    return RunPipe([cmd], capture=True)
> diff --git a/tools/scripts/patman/commit.py b/tools/scripts/patman/commit.py
> new file mode 100644
> index 0000000..059d372
> --- /dev/null
> +++ b/tools/scripts/patman/commit.py
> @@ -0,0 +1,77 @@
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +import re
> +
> +# Separates a tag: at the beginning of the subject from the rest of it
> +re_subject_tag = re.compile('([^:]*):\s*(.*)')
> +
> +class Commit:
> +    """Holds information about a single commit/patch in the series.
> +
> +    Args:
> +        hash: Commit hash (as a string)
> +
> +    Variables:
> +        hash: Commit hash
> +        subject: Subject line
> +        tags: List of maintainer tag strings
> +        changes: Dict containing a list of changes (single line strings).
> +            The dict is indexed by change version (an integer)
> +    """
> +    def __init__(self, hash):
> +        self.hash = hash
> +        self.subject = None
> +        self.tags = []
> +        self.changes = {}
> +
> +    def AddChange(self, version, info):
> +        """Add a new change line to the change list for a version.
> +
> +        Args:
> +            version: Patch set version (integer: 1, 2, 3)
> +            info: Description of change in this version
> +        """
> +        if not self.changes.get(version):
> +            self.changes[version] = []
> +        self.changes[version].append(info)
> +
> +    def CheckTags(self):
> +        """Create a list of subject tags in the commit
> +
> +        Subject tags look like this:
> +
> +            propounder: Change the widget to propound correctly
> +
> +        Multiple tags are supported. The list is updated in self.tag
> +
> +        Returns:
> +            None if ok, else the name of a tag with no email alias
> +        """
> +        str = self.subject
> +        m = True
> +        while m:
> +            m = re_subject_tag.match(str)
> +            if m:
> +                tag = m.group(1)
> +                self.tags.append(tag)
> +                str = m.group(2)
> +        return None
> diff --git a/tools/scripts/patman/gitutil.py b/tools/scripts/patman/gitutil.py
> new file mode 100644
> index 0000000..2c498e0
> --- /dev/null
> +++ b/tools/scripts/patman/gitutil.py
> @@ -0,0 +1,212 @@
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +import command
> +import re
> +import os
> +import settings
> +import subprocess
> +import sys
> +import terminal
> +
> +def CountCommitsToBranch():
> +    """Returns number of commits between HEAD and the tracking branch.
> +
> +    This looks back to the tracking branch and works out the number of commits
> +    since then.
> +
> +    TODO: Simplify this

You can use the "@{upstream}" syntax to ask git for a reference to the tracking
branch's head:

git log --pretty=oneline "@{upstream}"..HEAD

-Anton

> +    Return:
> +        Number of patches that exist on top of the branch
> +    """
> +    pipe = [['git', 'branch'], ['grep', '^*']]
> +    branch = command.RunPipe(pipe, capture=True, oneline=True).split(' ')[1]
> +
> +    pipe = [['git', 'config', '-l'], ['grep', '^branch\.%s' % branch]]
> +    stdout = command.RunPipe(pipe, capture=True)
> +    re_keyvalue = re.compile('(\w*)=(.*)')
> +    dict = {}
> +    for line in stdout.splitlines():
> +        m = re_keyvalue.search(line)
> +        dict[m.group(1)] = m.group(2)
> +    upstream_branch = dict['merge'].split('/')[-1]
> +
> +    pipe = [['git', 'log', '--oneline',
> +                'remotes/%s/%s..' % (dict['remote'], upstream_branch)],
> +            ['wc', '-l']]
> +    stdout = command.RunPipe(pipe, capture=True, oneline=True)
> +    patch_count =int(stdout)
> +    return patch_count
> +
> +def CreatePatches(start, count, series):
> +    """Create a series of patches from the top of the current branch.
> +
> +    The patch files are written to the current directory using
> +    git format-patch.
> +
> +    Args:
> +        start: Commit to start from: 0=HEAD, 1=next one, etc.
> +        count: number of commits to include
> +    Return:
> +        Filename of cover letter
> +        List of filenames of patch files
> +    """
> +    if series.get('version'):
> +        version = '%s ' % series['version']
> +    cmd = ['git', 'format-patch', '--signoff']
> +    if series.get('cover'):
> +        cmd.append('--cover-letter')
> +    prefix = series.GetPatchPrefix()
> +    if prefix:
> +        cmd += ['--subject-prefix=%s' % prefix]
> +    cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
> +
> +    stdout = command.RunList(cmd)
> +    files = stdout.splitlines()
> +
> +    # We have an extra file if there is a cover letter
> +    if series.get('cover'):
> +       return files[0], files[1:]
> +    else:
> +       return None, files
> +
> +def ApplyPatch(verbose, fname):
> +    """Apply a patch with git am to test it
> +
> +    TODO: Convert these to use command, with stderr option
> +
> +    Args:
> +        fname: filename of patch file to apply
> +    """
> +    cmd = ['git', 'am', fname]
> +    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
> +            stderr=subprocess.PIPE)
> +    stdout, stderr = pipe.communicate()
> +    re_error = re.compile('^error: patch failed: (.+):(\d+)')
> +    for line in stderr.splitlines():
> +        if verbose:
> +            print line
> +        match = re_error.match(line)
> +        if match:
> +            print GetWarningMsg('warning', match.group(1), int(match.group(2)),
> +                    'Patch failed')
> +    return pipe.returncode == 0, stdout
> +
> +def ApplyPatches(verbose, args, start_point):
> +    """Apply the patches with git am to make sure all is well
> +
> +    Args:
> +        verbose: Print out 'git am' output verbatim
> +        args: List of patch files to apply
> +        start_point: Number of commits back from HEAD to start applying.
> +            Normally this is len(args), but it can be larger if a start
> +            offset was given.
> +    """
> +    error_count = 0
> +    col = terminal.Color()
> +
> +    # Figure out our current position
> +    cmd = ['git', 'name-rev', 'HEAD', '--name-only']
> +    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
> +    stdout, stderr = pipe.communicate()
> +    if pipe.returncode:
> +        str = 'Could not find current commit name'
> +        print col.Color(col.RED, str)
> +        print stdout
> +        return False
> +    old_head = stdout.splitlines()[0]
> +
> +    # Checkout the required start point
> +    cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
> +    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
> +            stderr=subprocess.PIPE)
> +    stdout, stderr = pipe.communicate()
> +    if pipe.returncode:
> +        str = 'Could not move to commit before patch series'
> +        print col.Color(col.RED, str)
> +        print stdout, stderr
> +        return False
> +
> +    # Apply all the patches
> +    for fname in args:
> +        ok, stdout = ApplyPatch(verbose, fname)
> +        if not ok:
> +            print col.Color(col.RED, 'git am returned errors for %s: will '
> +                    'skip this patch' % fname)
> +            if verbose:
> +                print stdout
> +            error_count += 1
> +            cmd = ['git', 'am', '--skip']
> +            pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
> +            stdout, stderr = pipe.communicate()
> +            if pipe.returncode != 0:
> +                print col.Color(col.RED, 'Unable to skip patch! Aborting...')
> +                print stdout
> +                break
> +
> +    # Return to our previous position
> +    cmd = ['git', 'checkout', old_head]
> +    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
> +    stdout, stderr = pipe.communicate()
> +    if pipe.returncode:
> +        print col.Color(col.RED, 'Could not move back to head commit')
> +        print stdout, stderr
> +    return error_count == 0
> +
> +def EmailPatches(series, cover_fname, args, dry_run, cc_fname,
> +        self_only=False):
> +    """Email a patch series.
> +
> +    Args:
> +        series: Series object containing destination info
> +        cover_fname: filename of cover letter
> +        args: list of filenames of patch files
> +        dry_run: Just return the command that would be run
> +        cc_fname: Filename of Cc file for per-commit Cc
> +        self_only: True to just email to yourself as a test
> +
> +    Returns:
> +        Git command that was/would be run
> +    """
> +    dest = series.get('to')
> +    if not dest:
> +        print ("No recipient, please add something like this to a commit\n"
> +            "Series-to: Fred Bloggs <f.blogs at napier.co.nz>")
> +        return
> +    to = settings.LookupEmail(dest)
> +    cc = []
> +    for item in series.get('cc'):
> +        email = settings.LookupEmail(item)
> +        if email:
> +            cc += ['-cc', '"%s"' % email]
> +    if self_only:
> +        to = os.getenv('USER')
> +        cc = []
> +    cmd = ['git', 'send-email', '--annotate', '--to', '"%s"' % to]
> +    cmd += cc
> +    cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
> +    if cover_fname:
> +        cmd.append(cover_fname)
> +    cmd += args
> +    str = ' '.join(cmd)
> +    if not dry_run:
> +        os.system(str)
> +    return str
> diff --git a/tools/scripts/patman/patchstream.py b/tools/scripts/patman/patchstream.py
> new file mode 100644
> index 0000000..4727d57
> --- /dev/null
> +++ b/tools/scripts/patman/patchstream.py
> @@ -0,0 +1,426 @@
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +import os
> +import re
> +import shutil
> +import tempfile
> +
> +import command
> +import commit
> +import gitutil
> +from series import Series
> +
> +# Tags that we detect and remove
> +re_remove = re.compile('^BUG=|^TEST=|^Change-Id:|^Review URL:'
> +    '|Reviewed-on:|Reviewed-by:')
> +
> +# Lines which are allowed after a TEST= line
> +re_allowed_after_test = re.compile('^Signed-off-by:')
> +
> +# The start of the cover letter
> +re_cover = re.compile('^Cover-letter:')
> +
> +# Patch series tag
> +re_series = re.compile('^Series-(\w*): *(.*)')
> +
> +# Commit tags that we want to collect and keep
> +re_tag = re.compile('^(Tested-by|Acked-by|Signed-off-by): (.*)')
> +
> +# The start of a new commit in the git log
> +re_commit = re.compile('commit (.*)')
> +
> +# We detect these since checkpatch doesn't always do it
> +re_space_before_tab = re.compile('^[+].* \t')
> +
> +# States we can be in - can we use range() and still have comments?
> +STATE_MSG_HEADER = 0        # Still in the message header
> +STATE_PATCH_SUBJECT = 1     # In patch subject (first line of log for a commit)
> +STATE_PATCH_HEADER = 2      # In patch header (after the subject)
> +STATE_DIFFS = 3             # In the diff part (past --- line)
> +
> +class PatchStream:
> +    """Class for detecting/injecting tags in a patch or series of patches
> +
> +    We support processing the output of 'git log' to read out the tags we
> +    are interested in. We can also process a patch file in order to remove
> +    unwanted tags or inject additional ones. These correspond to the two
> +    phases of processing.
> +    """
> +    def __init__(self, series, name=None, is_log=False):
> +        self.skip_blank = False          # True to skip a single blank line
> +        self.found_test = False          # Found a TEST= line
> +        self.lines_after_test = 0        # MNumber of lines found after TEST=
> +        self.warn = []                   # List of warnings we have collected
> +        self.linenum = 1                 # Output line number we are up to
> +        self.in_section = None           # Name of start...END section we are in
> +        self.notes = []                  # Series notes
> +        self.section = []                # The current section...END section
> +        self.series = series             # Info about the patch series
> +        self.is_log = is_log             # True if indent like git log
> +        self.in_change = 0               # Non-zero if we are in a change list
> +        self.blank_count = 0             # Number of blank lines stored up
> +        self.state = STATE_MSG_HEADER    # What state are we in?
> +        self.tags = []                   # Tags collected, like Tested-by...
> +        self.signoff = None              # Contents of signoff line
> +        self.commit = None               # Current commit
> +
> +    def AddToSeries(self, line, name, value):
> +        """Add a new Series-xxx tag.
> +
> +        When a Series-xxx tag is detected, we come here to record it, if we
> +        are scanning a 'git log'.
> +
> +        Args:
> +            line: Source line containing tag (useful for debug/error messages)
> +            name: Tag name (part after 'Series-')
> +            value: Tag value (part after 'Series-xxx: ')
> +        """
> +        if name == 'notes':
> +            self.in_section = name
> +            self.skip_blank = False
> +        if self.is_log:
> +            self.series.AddTag(line, name, value)
> +
> +    def CloseCommit(self):
> +        """Save the current commit into our commit list, and reset our state"""
> +        if self.commit and self.is_log:
> +            self.series.AddCommit(self.commit)
> +            self.commit = None
> +
> +    def ProcessLine(self, line):
> +        """Process a single line of a patch file or commit log
> +
> +        This process a line and returns a list of lines to output. The list
> +        may be empty or may contain multiple output lines.
> +
> +        This is where all the complicated logic is located. The class's
> +        state is used to move between different states and detect things
> +        properly.
> +
> +        We can be in one of two modes:
> +            self.is_log == True: This is 'git log' mode, where most output is
> +                indented by 4 characters and we are scanning for tags
> +
> +            self.is_log == False: This is 'patch' mode, where we already have
> +                all the tags, and are processing patches to remove junk we
> +                don't want, and add things we think are required.
> +
> +        Args:
> +            line: text line to process
> +
> +        Returns:
> +            list of output lines, or [] if nothing should be output
> +        """
> +        # Initially we have no output. Prepare the input line string
> +        out = []
> +        line = line.rstrip('\n')
> +        if self.is_log:
> +            if line[:4] == '    ':
> +                line = line[4:]
> +
> +        # Handle state transition and skipping blank lines
> +        series_match = re_series.match(line)
> +        commit_match = re_commit.match(line) if self.is_log else None
> +        tag_match = None
> +        if self.state == STATE_PATCH_HEADER:
> +            tag_match = re_tag.match(line)
> +        is_blank = not line.strip()
> +        if is_blank:
> +            if (self.state == STATE_MSG_HEADER
> +                    or self.state == STATE_PATCH_SUBJECT):
> +                self.state += 1
> +
> +            # We don't have a subject in the text stream of patch files
> +            # It has its own line with a Subject: tag
> +            if not self.is_log and self.state == STATE_PATCH_SUBJECT:
> +                self.state += 1
> +        elif commit_match:
> +            self.state = STATE_MSG_HEADER
> +
> +        # If we are in a section, keep collecting lines until we see END
> +        if self.in_section:
> +            if line == 'END':
> +                if self.in_section == 'cover':
> +                    self.series.cover = self.section
> +                elif self.in_section == 'notes':
> +                    self.series.notes += self.section
> +                else:
> +                    self.warn.append("Unknown section '%s'" % self.in_section)
> +                self.in_section = None
> +                self.skip_blank = True
> +                self.section = []
> +            else:
> +                self.section.append(line)
> +
> +        # Detect the commit subject
> +        elif not is_blank and self.state == STATE_PATCH_SUBJECT:
> +            self.commit.subject = line
> +
> +        # Detect the tags we want to remove, and skip blank lines
> +        elif re_remove.match(line):
> +            self.skip_blank = True
> +
> +            # TEST= should be the last thing in the commit, so remove
> +            # everything after it
> +            if line.startswith('TEST='):
> +                self.found_test = True
> +        elif self.skip_blank and is_blank:
> +            self.skip_blank = False
> +
> +        # Detect the start of a cover letter section
> +        elif re_cover.match(line):
> +            self.in_section = 'cover'
> +            self.skip_blank = False
> +
> +        # If we are in a change list, key collected lines until a blank one
> +        elif self.in_change:
> +            if is_blank:
> +                # Blank line ends this change list
> +                self.in_change = 0
> +            else:
> +                self.series.AddChange(self.in_change, line)
> +            self.skip_blank = False
> +
> +        # Detect Series-xxx tags
> +        elif series_match:
> +            name = series_match.group(1)
> +            value = series_match.group(2)
> +            if name == 'changes':
> +                # value is the version number: e.g. 1, or 2
> +                value = int(value)
> +                self.in_change = int(value)
> +            else:
> +                self.AddToSeries(line, name, value)
> +                self.skip_blank = True
> +
> +        # Detect the start of a new commit
> +        elif commit_match:
> +            self.CloseCommit()
> +            self.commit = commit.Commit(commit_match.group(1)[:7])
> +
> +        # Detect tags in the commit message
> +        elif tag_match:
> +            # Onlly allow a single signoff tag
> +            if tag_match.group(1) == 'Signed-off-by':
> +                if self.signoff:
> +                    self.warn.append('Patch has more than one Signed-off-by '
> +                            'tag')
> +                else:
> +                    self.signoff = line
> +
> +            # Remove Tested-by self, since few will take much notice
> +            elif (tag_match.group(1) == 'Tested-by' and
> +                    tag_match.group(2).find(os.getenv('USER') + '@') != -1):
> +                self.warn.append("Ignoring %s" % line)
> +            else:
> +                self.tags.append(line)
> +
> +        # Well that means this is an ordinary line
> +        else:
> +            pos = 1
> +            # Look for ugly ASCII characters
> +            for ch in line:
> +                # TODO: Would be nicer to report source filename and line
> +                if ord(ch) > 0x80:
> +                    self.warn.append('Line %d/%d has funny ascii character' %
> +                        (self.linenum, pos))
> +                pos += 1
> +
> +            # Look for space before tab
> +            m = re_space_before_tab.match(line)
> +            if m:
> +                self.warn.append('Line %d/%d has space before tab' %
> +                    (self.linenum, m.start()))
> +
> +            # OK, we have a valid non-blank line
> +            out = [line]
> +            self.linenum += 1
> +            self.skip_blank = False
> +            if self.state == STATE_DIFFS:
> +                pass
> +
> +            # If this is the start of the diffs section, emit our tags and
> +            # change log
> +            elif line == '---':
> +                self.state = STATE_DIFFS
> +
> +                # Output the tags (signeoff first), then change list
> +                out = []
> +                if self.signoff:
> +                    out += [self.signoff]
> +                out += sorted(self.tags) + [line] + self.series.MakeChangeLog()
> +            elif self.found_test:
> +                if not re_allowed_after_test.match(line):
> +                    self.lines_after_test += 1
> +
> +        return out
> +
> +    def Finalize(self):
> +        """Close out processing of this patch stream"""
> +        self.CloseCommit()
> +        if self.lines_after_test:
> +            self.warn.append('Found %d lines after TEST=' %
> +                    self.lines_after_test)
> +
> +    def ProcessStream(self, infd, outfd):
> +        """Copy a stream from infd to outfd, filtering out unwanting things.
> +
> +        This is used to process patch files one at a time.
> +
> +        Args:
> +            infd: Input stream file object
> +            outfd: Output stream file object
> +        """
> +        # Extract the filename from each diff, for nice warnings
> +        fname = None
> +        last_fname = None
> +        re_fname = re.compile('diff --git a/(.*) b/.*')
> +        while True:
> +            line = infd.readline()
> +            if not line:
> +                break
> +            out = self.ProcessLine(line)
> +
> +            # Try to detect blank lines at EOF
> +            for line in out:
> +                match = re_fname.match(line)
> +                if match:
> +                    last_fname = fname
> +                    fname = match.group(1)
> +                if line == '+':
> +                    self.blank_count += 1
> +                else:
> +                    if self.blank_count and (line == '-- ' or match):
> +                        self.warn.append("Found possible blank line(s) at "
> +                                "end of file '%s'" % last_fname)
> +                    outfd.write('+\n' * self.blank_count)
> +                    outfd.write(line + '\n')
> +                    self.blank_count = 0
> +        self.Finalize()
> +
> +
> +def GetMetaData(start, count):
> +    """Reads out patch series metadata from the commits
> +
> +    This does a 'git log' on the relevant commits and pulls out the tags we
> +    are interested in.
> +
> +    Args:
> +        start: Commit to start from: 0=HEAD, 1=next one, etc.
> +        count: Number of commits to list
> +    """
> +    pipe = [['git', 'log', '--reverse', 'HEAD~%d' % start, '-n%d' % count]]
> +    stdout = command.RunPipe(pipe, capture=True)
> +    series = Series()
> +    ps = PatchStream(series, is_log=True)
> +    for line in stdout.splitlines():
> +        ps.ProcessLine(line)
> +    ps.Finalize()
> +    return series
> +
> +def FixPatch(backup_dir, fname, series, commit):
> +    """Fix up a patch file, by adding/removing as required.
> +
> +    We remove our tags from the patch file, insert changes lists, etc.
> +    The patch file is processed in place, and overwritten.
> +
> +    A backup file is put into backup_dir (if not None).
> +
> +    Args:
> +        fname: Filename to patch file to process
> +        series: Series information about this patch set
> +        commit: Commit object for this patch file
> +    Return:
> +        A list of errors, or [] if all ok.
> +    """
> +    handle, tmpname = tempfile.mkstemp()
> +    outfd = os.fdopen(handle, 'w')
> +    infd = open(fname, 'r')
> +    ps = PatchStream(series)
> +    ps.commit = commit
> +    ps.ProcessStream(infd, outfd)
> +    infd.close()
> +    outfd.close()
> +
> +    # Create a backup file if required
> +    if backup_dir:
> +        shutil.copy(fname, os.path.join(backup_dir, os.path.basename(fname)))
> +    shutil.move(tmpname, fname)
> +    return ps.warn
> +
> +def FixPatches(series, fnames):
> +    """Fix up a list of patches identified by filenames
> +
> +    The patch files are processed in place, and overwritten.
> +
> +    Args:
> +        series: The series object
> +        fnames: List of patch files to process
> +    """
> +    # Current workflow creates patches, so we shouldn't need a backup
> +    backup_dir = None  #tempfile.mkdtemp('clean-patch')
> +    count = 0
> +    for fname in fnames:
> +        commit = series.commits[count]
> +        commit.patch = fname
> +        result = FixPatch(backup_dir, fname, series, commit)
> +        if result:
> +            print '%d warnings for %s:' % (len(result), fname)
> +            for warn in result:
> +                print '\t', warn
> +            print
> +        count += 1
> +    print 'Cleaned %d patches' % count
> +    return series
> +
> +def InsertCoverLetter(fname, series, count):
> +    """Inserts a cover letter with the required info into patch 0
> +
> +    Args:
> +        fname: Input / output filename of the cover letter file
> +        series: Series object
> +        count: Number of patches in the series
> +    """
> +    fd = open(fname, 'r')
> +    lines = fd.readlines()
> +    fd.close()
> +
> +    fd = open(fname, 'w')
> +    text = series.cover
> +    prefix = series.GetPatchPrefix()
> +    for line in lines:
> +        if line.startswith('Subject:'):
> +            # TODO: if more than 10 patches this should save 00/xx, not 0/xx
> +            line = 'Subject: [%s 0/%d] %s\n' % (prefix, count, text[0])
> +
> +        # Insert our cover letter
> +        elif line.startswith('*** BLURB HERE ***'):
> +            # First the blurb test
> +            line = '\n'.join(text[1:]) + '\n'
> +            if series.get('notes'):
> +                line += '\n'.join(series.notes) + '\n'
> +
> +            # Now the change list
> +            out = series.MakeChangeLog()
> +            line += '\n' + '\n'.join(out)
> +        fd.write(line)
> +    fd.close()
> diff --git a/tools/scripts/patman/patman b/tools/scripts/patman/patman
> new file mode 120000
> index 0000000..6cc3d7a
> --- /dev/null
> +++ b/tools/scripts/patman/patman
> @@ -0,0 +1 @@
> +patman.py
> \ No newline at end of file
> diff --git a/tools/scripts/patman/patman.py b/tools/scripts/patman/patman.py
> new file mode 100755
> index 0000000..051b9ac
> --- /dev/null
> +++ b/tools/scripts/patman/patman.py
> @@ -0,0 +1,127 @@
> +#!/usr/bin/python
> +#
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +"""See README for more information"""
> +
> +from optparse import OptionParser
> +import os
> +import re
> +import sys
> +import unittest
> +
> +# Our modules
> +import checkpatch
> +import command
> +import gitutil
> +import patchstream
> +import settings
> +import terminal
> +import test
> +
> +
> +parser = OptionParser()
> +parser.add_option('-t', '--test', action='store_true', dest='test',
> +                  default=False, help='run tests')
> +parser.add_option('-c', '--count', dest='count', type='int',
> +       default=-1, help='Automatically create patches from top n commits')
> +parser.add_option('-s', '--start', dest='start', type='int',
> +       default=0, help='Commit to start creating patches from (0 = HEAD)')
> +parser.add_option('-n', '--dry-run', action='store_true', dest='dry_run',
> +       default=False, help='Do a try run by emailing to yourself')
> +parser.add_option('-i', '--ignore-errors', action='store_true',
> +       dest='ignore_errors', default=False,
> +       help='Send patches email even if patch errors are found')
> +parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
> +       default=False, help='Verbose output of errors and warnings')
> +parser.add_option('--cc-cmd', dest='cc_cmd', type='string', action='store',
> +       default=None, help='Output cc list for patch file (used by git)')
> +
> +(options, args) = parser.parse_args()
> +
> +# Run our meagre tests
> +if options.test:
> +    sys.argv = [sys.argv[0]]
> +    suite = unittest.TestLoader().loadTestsFromTestCase(test.TestPatch)
> +    result = unittest.TestResult()
> +    suite.run(result)
> +
> +    # TODO: Surely we can just 'print' result?
> +    print result
> +    for test, err in result.errors:
> +        print err
> +
> +# Called from git with a patch filename as argument
> +# Printout a list of additional CC recipients for this patch
> +elif options.cc_cmd:
> +    fd = open(options.cc_cmd, 'r')
> +    re_line = re.compile('(\S*) (.*)')
> +    for line in fd.readlines():
> +        match = re_line.match(line)
> +        if match and match.lastgroup >= 1 and match.group(1) == args[0]:
> +            for cc in match.group(2).split(', '):
> +                print cc
> +    fd.close()
> +
> +# Process commits, produce patches files, check them, email them
> +else:
> +    if options.count == -1:
> +        # Work out how many patches to send if we can
> +        options.count = gitutil.CountCommitsToBranch() - options.start
> +
> +    col = terminal.Color()
> +    if not options.count:
> +        str = 'No commits found to process - please use -c flag'
> +        print col.Color(col.RED, str)
> +        sys.exit(1)
> +
> +    # Read the metadata from the commits
> +    if options.count:
> +        series = patchstream.GetMetaData(options.start, options.count)
> +        cover_fname, args = gitutil.CreatePatches(options.start, options.count,
> +                series)
> +
> +    # Fix up the patch files to our liking, and insert the cover letter
> +    series = patchstream.FixPatches(series, args)
> +    if series and cover_fname and series.get('cover'):
> +        patchstream.InsertCoverLetter(cover_fname, series, options.count)
> +
> +    # Do a few checks on the series
> +    series.DoChecks()
> +
> +    # Check the patches, and run them through 'git am' just to be sure
> +    ok = checkpatch.CheckPatches(options.verbose, args)
> +    if not gitutil.ApplyPatches(options.verbose, args,
> +            options.count + options.start):
> +        ok = False
> +
> +    # Email the patches out (giving the user time to check / cancel)
> +    cmd = ''
> +    if ok or options.ignore_errors:
> +        cc_file = series.MakeCcFile()
> +        cmd = gitutil.EmailPatches(series, cover_fname, args,
> +                options.dry_run, cc_file)
> +        os.remove(cc_file)
> +
> +    # For a dry run, just show our actions as a sanity check
> +    if options.dry_run:
> +        series.ShowActions(args, cmd)
> diff --git a/tools/scripts/patman/series.py b/tools/scripts/patman/series.py
> new file mode 100644
> index 0000000..bc2b370
> --- /dev/null
> +++ b/tools/scripts/patman/series.py
> @@ -0,0 +1,232 @@
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +import os
> +
> +import settings
> +import terminal
> +
> +# Series-xxx tags that we understand
> +valid_series = ['to', 'cc', 'version', 'changes', 'prefix', 'notes'];
> +
> +class Series(dict):
> +    """Holds information about a patch series, including all tags.
> +
> +    Vars:
> +        cc: List of aliases/emails to Cc all patches to
> +        commits: List of Commit objects, one for each patch
> +        cover: List of lines in the cover letter
> +        notes: List of lines in the notes
> +        changes: (dict) List of changes for each version, The key is
> +            the integer version number
> +    """
> +    def __init__(self):
> +        self.cc = []
> +        self.commits = []
> +        self.cover = None
> +        self.notes = []
> +        self.changes = {}
> +
> +    # These make us more like a dictionary
> +    def __setattr__(self, name, value):
> +        self[name] = value
> +
> +    def __getattr__(self, name):
> +        return self[name]
> +
> +    def AddTag(self, line, name, value):
> +        """Add a new Series-xxx tag along with its value.
> +
> +        Args:
> +            line: Source line containing tag (useful for debug/error messages)
> +            name: Tag name (part after 'Series-')
> +            value: Tag value (part after 'Series-xxx: ')
> +        """
> +        # If we already have it, then add to our list
> +        if name in self:
> +            values = value.split(',')
> +            values = [str.strip() for str in values]
> +            if type(self[name]) != type([]):
> +                raise ValueError("In %s: line '%s': Cannot add another value "
> +                        "'%s' to series '%s'" %
> +                            (self.commit.hash, line, values, self[name]))
> +            self[name] += values
> +
> +        # Otherwise just set the value
> +        elif name in valid_series:
> +            self[name] = value
> +        else:
> +            raise ValueError("In %s: line '%s': Unknown 'Series-%s': valid "
> +                        "options are %s" % (self.commit.hash, line, name,
> +                            ', '.join(valid_series)))
> +
> +    def AddCommit(self, commit):
> +        """Add a commit into our list of commits
> +
> +        We create a list of tags in the commit subject also.
> +
> +        Args:
> +            commit: Commit object to add
> +        """
> +        commit.CheckTags()
> +        self.commits.append(commit)
> +
> +    def ShowActions(self, args, cmd):
> +        """Show what actions we will/would perform
> +
> +        Args:
> +            args: List of patch files we created
> +            cmd: The git command we would have run
> +        """
> +        col = terminal.Color()
> +        print 'Dry run, so not doing much. But I would do this:'
> +        print
> +        print 'Send a total of %d patch%s with %scover letter.' % (
> +                len(args), '' if len(args) == 1 else 'es',
> +                self.get('cover') and 'a ' or 'no ')
> +
> +        # TODO: Colour the patches according to whether they passed checks
> +        for upto in range(len(args)):
> +            commit = self.commits[upto]
> +            print col.Color(col.GREEN, '   %s' % args[upto])
> +            for tag in commit.tags:
> +                email = settings.LookupEmail(tag)
> +                if email == None:
> +                    email = col.Color(col.YELLOW, "<alias '%s' not found>"
> +                            % tag)
> +                if email:
> +                    print '      cc: ',email
> +        print
> +        print 'To:\t ', self.get('to', '<none>')
> +        for item in self.cc:
> +            print 'Cc:\t ', settings.LookupEmail(item)
> +        print 'Version: ', self.get('version')
> +        print 'Prefix:\t ', self.get('prefix')
> +        if self.cover:
> +            print 'Cover: %d lines' % len(self.cover)
> +        if cmd:
> +            print 'Git command: %s' % cmd
> +
> +    def MakeChangeLog(self):
> +        """Create a list of changes for each version.
> +
> +        Return:
> +            The change log as a list of strings, one per line
> +
> +            Changes in v1:
> +            - Fix the widget
> +            - Jog the dial
> +
> +            Changes in v2:
> +            - Jog the dial back closer to the widget
> +
> +            etc.
> +        """
> +        final = []
> +        need_blank = False
> +        for change in sorted(self.changes):
> +            out = []
> +            if need_blank:
> +                out.append('')
> +            out.append('Changes in v%d:' % change)
> +            for item in self.changes[change]:
> +                if item not in out:
> +                    out.append(item)
> +            need_blank = True
> +            final += out
> +        if self.changes:
> +            final.append('')
> +        return final
> +
> +    def DoChecks(self):
> +        """Check that each version has a change log
> +
> +        Print an error if something is wrong.
> +        """
> +        col = terminal.Color()
> +        if self.get('version'):
> +            changes_copy = dict(self.changes)
> +            for version in range(2, int(self.version) + 1):
> +                if self.changes.get(version):
> +                    del changes_copy[version]
> +                else:
> +                    str = 'Change log missing for v%d' % version
> +                    print col.Color(col.RED, str)
> +            for version in changes_copy:
> +                str = 'Change log for unknown version v%d' % version
> +                print col.Color(col.RED, str)
> +        elif self.changes:
> +            str = 'Change log exists, but no version is set'
> +            print col.Color(col.RED, str)
> +
> +    def MakeCcFile(self):
> +        """Make a cc file for us to use for per-commit Cc automation
> +
> +        Return:
> +            Filename of temp file created
> +        """
> +        # Look for commit tags (of the form 'xxx:' at the start of the subject)
> +        fname = '/tmp/cleanpatch.%d' % os.getpid()
> +        fd = open(fname, 'w')
> +        for commit in self.commits:
> +            list = []
> +            for tag in commit.tags:
> +                alias = settings.LookupEmail(tag)
> +
> +                # We allow empty aliases - these are silently ignored
> +                if alias is not None:
> +                    list.append(alias)
> +                elif alias == '':
> +                    pass
> +                else:
> +                    print "Tag '%s' not found" % tag
> +            print >>fd, commit.patch, ', '.join(list)
> +        fd.close()
> +        return fname
> +
> +    def AddChange(self, version, info):
> +        """Add a new change line to a version.
> +
> +        This will later appear in the change log.
> +
> +        Args:
> +            version: version number to add change list to
> +            info: change line for this version
> +        """
> +        if not self.changes.get(version):
> +            self.changes[version] = []
> +        self.changes[version].append(info)
> +
> +    def GetPatchPrefix(self):
> +        """Get the patch version string
> +
> +        Return:
> +            Patch string, like 'RFC PATCH v5' or just 'PATCH'
> +        """
> +        version = ''
> +        if self.get('version'):
> +            version = ' v%s' % self['version']
> +
> +        # Get patch name prefix
> +        prefix = ''
> +        if self.get('prefix'):
> +            prefix = '%s ' % self['prefix']
> +        return '%sPATCH%s' % (prefix, version)
> diff --git a/tools/scripts/patman/settings.py b/tools/scripts/patman/settings.py
> new file mode 100644
> index 0000000..aa17585
> --- /dev/null
> +++ b/tools/scripts/patman/settings.py
> @@ -0,0 +1,50 @@
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +import ConfigParser
> +import os
> +
> +settings = ConfigParser.SafeConfigParser()
> +settings.read('%s/.config/patman' % os.getenv('HOME'))
> +
> +def LookupEmail(lookup_name):
> +    """If an email address is an alias, look it up and return the full name
> +
> +    TODO: Why not just use git's own alias feature?
> +
> +    Args:
> +        lookup_name: Name or email address to look up
> +
> +    Returns:
> +        name, if it is a valid email address,
> +            or real name, if name is an alias (may be blank),
> +            or None if it was not found in the alias file
> +    """
> +    if '@' in lookup_name: # Perhaps a real email address
> +        return lookup_name
> +
> +    #if self.has_section('alias'):
> +    for name, value in settings.items('alias'):
> +        if name == lookup_name:
> +            return value
> +
> +    #print "No match for alias '%s'" % name
> +    return None
> diff --git a/tools/scripts/patman/terminal.py b/tools/scripts/patman/terminal.py
> new file mode 100644
> index 0000000..838c828
> --- /dev/null
> +++ b/tools/scripts/patman/terminal.py
> @@ -0,0 +1,86 @@
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +"""Terminal utilities
> +
> +This module handles terminal interaction including ANSI color codes.
> +"""
> +
> +class Color(object):
> +  """Conditionally wraps text in ANSI color escape sequences."""
> +  BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
> +  BOLD = -1
> +  COLOR_START = '\033[1;%dm'
> +  BOLD_START = '\033[1m'
> +  RESET = '\033[0m'
> +
> +  def __init__(self, enabled=True):
> +    """Create a new Color object, optionally disabling color output.
> +
> +    Args:
> +      enabled: True if color output should be enabled. If False then this
> +        class will not add color codes at all.
> +    """
> +    self._enabled = enabled
> +
> +  def Start(self, color):
> +    """Returns a start color code.
> +
> +    Args:
> +      color: Color to use, .e.g BLACK, RED, etc.
> +
> +    Returns:
> +      If color is enabled, returns an ANSI sequence to start the given color,
> +      otherwise returns empty string
> +    """
> +    if self._enabled:
> +      return self.COLOR_START % (color + 30)
> +    return ''
> +
> +  def Stop(self):
> +    """Retruns a stop color code.
> +
> +    Returns:
> +      If color is enabled, returns an ANSI color reset sequence, otherwise
> +      returns empty string
> +    """
> +    if self._enabled:
> +      return self.RESET
> +    return ''
> +
> +  def Color(self, color, text):
> +    """Returns text with conditionally added color escape sequences.
> +
> +    Keyword arguments:
> +      color: Text color -- one of the color constants defined in this class.
> +      text: The text to color.
> +
> +    Returns:
> +      If self._enabled is False, returns the original text. If it's True,
> +      returns text with color escape sequences based on the value of color.
> +    """
> +    if not self._enabled:
> +      return text
> +    if color == self.BOLD:
> +      start = self.BOLD_START
> +    else:
> +      start = self.COLOR_START % (color + 30)
> +    return start + text + self.RESET
> diff --git a/tools/scripts/patman/test.py b/tools/scripts/patman/test.py
> new file mode 100644
> index 0000000..5b2ccc7
> --- /dev/null
> +++ b/tools/scripts/patman/test.py
> @@ -0,0 +1,248 @@
> +#
> +# Copyright (c) 2011 The Chromium OS Authors.
> +#
> +# See file CREDITS for list of people who contributed to this
> +# project.
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> +# published by the Free Software Foundation; either version 2 of
> +# the License, or (at your option) any later version.
> +#
> +# 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
> +#
> +
> +import os
> +import tempfile
> +import unittest
> +
> +import checkpatch
> +import patchstream
> +import series
> +
> +
> +class TestPatch(unittest.TestCase):
> +    """Test this program
> +
> +    TODO: Write tests for the rest of the functionality
> +    """
> +
> +    def testBasic(self):
> +        """Test basic filter operation"""
> +        data='''
> +
> +From 656c9a8c31fa65859d924cd21da920d6ba537fad Mon Sep 17 00:00:00 2001
> +From: Simon Glass <sjg at chromium.org>
> +Date: Thu, 28 Apr 2011 09:58:51 -0700
> +Subject: [PATCH (resend) 3/7] Tegra2: Add more clock support
> +
> +This adds functions to enable/disable clocks and reset to on-chip peripherals.
> +
> +BUG=chromium-os:13875
> +TEST=build U-Boot for Seaboard, boot
> +
> +Change-Id: I80fe1d0c0b7dd10aa58ce5bb1d9290b6664d5413
> +
> +Review URL: http://codereview.chromium.org/6900006
> +
> +Signed-off-by: Simon Glass <sjg at chromium.org>
> +---
> + arch/arm/cpu/armv7/tegra2/Makefile         |    2 +-
> + arch/arm/cpu/armv7/tegra2/ap20.c           |   57 ++----
> + arch/arm/cpu/armv7/tegra2/clock.c          |  163 +++++++++++++++++
> +'''
> +        expected='''
> +
> +From 656c9a8c31fa65859d924cd21da920d6ba537fad Mon Sep 17 00:00:00 2001
> +From: Simon Glass <sjg at chromium.org>
> +Date: Thu, 28 Apr 2011 09:58:51 -0700
> +Subject: [PATCH (resend) 3/7] Tegra2: Add more clock support
> +
> +This adds functions to enable/disable clocks and reset to on-chip peripherals.
> +
> +Signed-off-by: Simon Glass <sjg at chromium.org>
> +---
> + arch/arm/cpu/armv7/tegra2/Makefile         |    2 +-
> + arch/arm/cpu/armv7/tegra2/ap20.c           |   57 ++----
> + arch/arm/cpu/armv7/tegra2/clock.c          |  163 +++++++++++++++++
> +'''
> +        out = ''
> +        inhandle, inname = tempfile.mkstemp()
> +        infd = os.fdopen(inhandle, 'w')
> +        infd.write(data)
> +        infd.close()
> +
> +        exphandle, expname = tempfile.mkstemp()
> +        expfd = os.fdopen(exphandle, 'w')
> +        expfd.write(expected)
> +        expfd.close()
> +
> +        patchstream.FixPatch(None, inname, series.Series(), None)
> +        rc = os.system('diff -u %s %s' % (inname, expname))
> +        self.assertEqual(rc, 0)
> +
> +        os.remove(inname)
> +        os.remove(expname)
> +
> +    def GetData(self, data_type):
> +        data='''
> +From 4924887af52713cabea78420eff03badea8f0035 Mon Sep 17 00:00:00 2001
> +From: Simon Glass <sjg at chromium.org>
> +Date: Thu, 7 Apr 2011 10:14:41 -0700
> +Subject: [PATCH 1/4] Add microsecond boot time measurement
> +
> +This defines the basics of a new boot time measurement feature. This allows
> +logging of very accurate time measurements as the boot proceeds, by using
> +an available microsecond counter.
> +
> +%s
> +---
> + README              |   11 ++++++++
> + common/bootstage.c  |   50 ++++++++++++++++++++++++++++++++++++
> + include/bootstage.h |   71 +++++++++++++++++++++++++++++++++++++++++++++++++++
> + include/common.h    |    8 ++++++
> + 5 files changed, 141 insertions(+), 0 deletions(-)
> + create mode 100644 common/bootstage.c
> + create mode 100644 include/bootstage.h
> +
> +diff --git a/README b/README
> +index 6f3748d..f9e4e65 100644
> +--- a/README
> ++++ b/README
> +@@ -2026,6 +2026,17 @@ The following options need to be configured:
> +               example, some LED's) on your board. At the moment,
> +               the following checkpoints are implemented:
> +
> ++- Time boot progress
> ++              CONFIG_BOOTSTAGE
> ++
> ++              Define this option to enable microsecond boot stage timing
> ++              on supported platforms. For this to work your platform
> ++              needs to define a function timer_get_us() which returns the
> ++              number of microseconds since reset. This would normally
> ++              be done in your SOC or board timer.c file.
> ++
> ++              You can add calls to bootstage_mark() to set time markers.
> ++
> + - Standalone program support:
> +               CONFIG_STANDALONE_LOAD_ADDR
> +
> +diff --git a/common/bootstage.c b/common/bootstage.c
> +new file mode 100644
> +index 0000000..2234c87
> +--- /dev/null
> ++++ b/common/bootstage.c
> +@@ -0,0 +1,50 @@
> ++/*
> ++ * Copyright (c) 2011, Google Inc. All rights reserved.
> ++ *
> ++ * See file CREDITS for list of people who contributed to this
> ++ * project.
> ++ *
> ++ * This program is free software; you can redistribute it and/or
> ++ * modify it under the terms of the GNU General Public License as
> ++ * published by the Free Software Foundation; either version 2 of
> ++ * the License, or (at your option) any later version.
> ++ *
> ++ * 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
> ++ */
> ++
> ++
> ++/*
> ++ * This module records the progress of boot and arbitrary commands, and
> ++ * permits accurate timestamping of each. The records can optionally be
> ++ * passed to kernel in the ATAGs
> ++ */
> ++
> ++#include <common.h>
> ++
> ++
> ++struct bootstage_record {
> ++      uint32_t time_us;
> ++      const char *name;
> ++};
> ++
> ++static struct bootstage_record record[BOOTSTAGE_COUNT];
> ++
> ++uint32_t bootstage_mark(enum bootstage_id id, const char *name)
> ++{
> ++      struct bootstage_record *rec = &record[id];
> ++
> ++      /* Only record the first event for each */
> ++%sif (!rec->name) {
> ++              rec->time_us = (uint32_t)timer_get_us();
> ++              rec->name = name;
> ++      }
> ++%sreturn rec->time_us;
> ++}
> +--
> +1.7.3.1
> +'''
> +        signoff = 'Signed-off-by: Simon Glass <sjg at chromium.org>\n'
> +        tab = '        '
> +        if data_type == 'good':
> +            pass
> +        elif data_type == 'no-signoff':
> +            signoff = ''
> +        elif data_type == 'spaces':
> +            tab = '   '
> +        else:
> +            print 'not implemented'
> +        return data % (signoff, tab, tab)
> +
> +    def SetupData(self, data_type):
> +        inhandle, inname = tempfile.mkstemp()
> +        infd = os.fdopen(inhandle, 'w')
> +        data = self.GetData(data_type)
> +        infd.write(data)
> +        infd.close()
> +        return inname
> +
> +    def testCheckpatch(self):
> +        """Test checkpatch operation"""
> +        inf = self.SetupData('good')
> +        result, problems, err, warn, lines, stdout = checkpatch.CheckPatch(inf)
> +        self.assertEqual(result, True)
> +        self.assertEqual(problems, [])
> +        self.assertEqual(err, 0)
> +        self.assertEqual(warn, 0)
> +        self.assertEqual(lines, 67)
> +        os.remove(inf)
> +
> +        inf = self.SetupData('no-signoff')
> +        result, problems, err, warn, lines, stdout = checkpatch.CheckPatch(inf)
> +        self.assertEqual(result, False)
> +        self.assertEqual(len(problems), 1)
> +        self.assertEqual(err, 1)
> +        self.assertEqual(warn, 0)
> +        self.assertEqual(lines, 67)
> +        os.remove(inf)
> +
> +        inf = self.SetupData('spaces')
> +        result, problems, err, warn, lines, stdout = checkpatch.CheckPatch(inf)
> +        self.assertEqual(result, False)
> +        self.assertEqual(len(problems), 2)
> +        self.assertEqual(err, 0)
> +        self.assertEqual(warn, 2)
> +        self.assertEqual(lines, 67)
> +        os.remove(inf)
> +
> +
> +if __name__ == "__main__":
> +    unittest.main()
> --
> 1.7.3.1
>
> _______________________________________________
> U-Boot mailing list
> U-Boot at lists.denx.de
> http://lists.denx.de/mailman/listinfo/u-boot
>


More information about the U-Boot mailing list