[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