[U-Boot] [RFC PATCH 0/1] Patch submission script

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@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:
[alias] me: Simon Glass sjg@chromium.org
u-boot: U-Boot Mailing List u-boot@lists.denx.de mikef: Mike Frysinger vapier@gentoo.org wolfgang: Wolfgang Denk wd@denx.de albert: Albert ARIBAUD albert.u.boot@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. They must appear in the first column of a line.
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.
There might be a few other features not mentioned in this README. They might be bugs.
Simon Glass (1): Add patch submission script all in one commit
tools/scripts/patman/.gitignore | 1 + tools/scripts/patman/README | 202 +++++++++++++++++ tools/scripts/patman/command.py | 72 ++++++ tools/scripts/patman/commit.py | 77 +++++++ tools/scripts/patman/gitutil.py | 210 +++++++++++++++++ tools/scripts/patman/patchstream.py | 426 +++++++++++++++++++++++++++++++++++ tools/scripts/patman/patman | 1 + tools/scripts/patman/patman.py | 127 +++++++++++ tools/scripts/patman/series.py | 220 ++++++++++++++++++ tools/scripts/patman/settings.py | 50 ++++ tools/scripts/patman/terminal.py | 86 +++++++ tools/scripts/patman/test.py | 248 ++++++++++++++++++++ 12 files changed, 1720 insertions(+), 0 deletions(-) create mode 100644 tools/scripts/patman/.gitignore create mode 100644 tools/scripts/patman/README 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

This is a script for automating submission of patches to the U-Boot mailing list.
Signed-off-by: Simon Glass sjg@chromium.org --- tools/scripts/patman/.gitignore | 1 + tools/scripts/patman/README | 202 +++++++++++++++++ tools/scripts/patman/command.py | 72 ++++++ tools/scripts/patman/commit.py | 77 +++++++ tools/scripts/patman/gitutil.py | 210 +++++++++++++++++ tools/scripts/patman/patchstream.py | 426 +++++++++++++++++++++++++++++++++++ tools/scripts/patman/patman | 1 + tools/scripts/patman/patman.py | 127 +++++++++++ tools/scripts/patman/series.py | 220 ++++++++++++++++++ tools/scripts/patman/settings.py | 50 ++++ tools/scripts/patman/terminal.py | 86 +++++++ tools/scripts/patman/test.py | 248 ++++++++++++++++++++ 12 files changed, 1720 insertions(+), 0 deletions(-) create mode 100644 tools/scripts/patman/.gitignore create mode 100644 tools/scripts/patman/README 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@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@chromium.org + +u-boot: U-Boot Mailing List u-boot@lists.denx.de +mikef: Mike Frysinger vapier@gentoo.org +wolfgang: Wolfgang Denk wd@denx.de +albert: Albert ARIBAUD albert.u.boot@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@chromium.org +19-Oct-11 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..d068f24 --- /dev/null +++ b/tools/scripts/patman/gitutil.py @@ -0,0 +1,210 @@ +# 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 + 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@napier.co.nz") + return + to = settings.LookupEmail(dest) + cc = [] + for item in series.get('cc'): + cc += ['-cc', '"%s"' % settings.LookupEmail(item)] + 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..a738511 --- /dev/null +++ b/tools/scripts/patman/series.py @@ -0,0 +1,220 @@ +# 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 + +# 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 + """ + 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 ') + for upto in range(len(args)): + commit = self.commits[upto] + print ' %s' % args[upto] + for tag in commit.tags: + email = settings.LookupEmail(tag) + print ' cc: ', (email if email else + "<alias '%s' not found>" % tag) + 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. + """ + 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) + if alias: + list.append(alias) + 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..6d59d15 --- /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, + or None if neither + """ + 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@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@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@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@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@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@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()
participants (1)
-
Simon Glass