2015-05-03

This tool is a Python script which:

- Creates patch directly from your branch

- Cleans them up by removing unwanted tags

- Inserts a cover letter with change lists

- Runs the patches through checkpatch.pl and its own checks

- Optionally emails them out to selected people

Add the main part of the code, excluding tests and documentation.

Signed-off-by: Simon Glass <sjg@chromium.org>

---

tools/patman/.gitignore | 1 +

tools/patman/checkpatch.py | 173 ++++++++++++

tools/patman/command.py | 123 +++++++++

tools/patman/commit.py | 88 ++++++

tools/patman/cros_subprocess.py | 397 +++++++++++++++++++++++++++

tools/patman/get_maintainer.py | 47 ++++

tools/patman/gitutil.py | 582 ++++++++++++++++++++++++++++++++++++++++

tools/patman/patchstream.py | 488 +++++++++++++++++++++++++++++++++

tools/patman/patman | 1 +

tools/patman/patman.py | 167 ++++++++++++

tools/patman/project.py | 27 ++

tools/patman/series.py | 271 +++++++++++++++++++

tools/patman/settings.py | 295 ++++++++++++++++++++

tools/patman/terminal.py | 158 +++++++++++

14 files changed, 2818 insertions(+)

create mode 100644 tools/patman/.gitignore

create mode 100644 tools/patman/checkpatch.py

create mode 100644 tools/patman/command.py

create mode 100644 tools/patman/commit.py

create mode 100644 tools/patman/cros_subprocess.py

create mode 100644 tools/patman/get_maintainer.py

create mode 100644 tools/patman/gitutil.py

create mode 100644 tools/patman/patchstream.py

create mode 120000 tools/patman/patman

create mode 100755 tools/patman/patman.py

create mode 100644 tools/patman/project.py

create mode 100644 tools/patman/series.py

create mode 100644 tools/patman/settings.py

create mode 100644 tools/patman/terminal.py

diff --git a/tools/patman/.gitignore b/tools/patman/.gitignore

new file mode 100644

index 0000000..0d20b64

--- /dev/null

+++ b/tools/patman/.gitignore

@@ -0,0 +1 @@

+*.pyc

diff --git a/tools/patman/checkpatch.py b/tools/patman/checkpatch.py

new file mode 100644

index 0000000..34a3bd2

--- /dev/null

+++ b/tools/patman/checkpatch.py

@@ -0,0 +1,173 @@

+# Copyright (c) 2011 The Chromium OS Authors.

+#

+# SPDX-License-Identifier: GPL-2.0+

+#

+

+import collections

+import command

+import gitutil

+import os

+import re

+import sys

+import terminal

+

+def FindCheckPatch():

+ top_level = gitutil.GetTopLevel()

+ try_list = [

+ os.getcwd(),

+ os.path.join(os.getcwd(), '..', '..'),

+ os.path.join(top_level, 'tools'),

+ os.path.join(top_level, 'scripts'),

+ '%s/bin' % os.getenv('HOME'),

+ ]

+ # Look in current dir

+ for path in try_list:

+ 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)

+

+ sys.exit('Cannot find checkpatch.pl - please put it in your ' +

+ '~/bin directory or use --no-check')

+

+def CheckPatch(fname, verbose=False):

+ """Run checkpatch.pl on a file.

+

+ Returns:

+ namedtuple containing:

+ ok: False=failure, True=ok

+ problems: List of problems, each a dict:

+ 'type'; error or warning

+ 'msg': text message

+ 'file' : filename

+ 'line': line number

+ errors: Number of errors

+ warnings: Number of warnings

+ checks: Number of checks

+ lines: Number of lines

+ stdout: Full output of checkpatch

+ """

+ fields = ['ok', 'problems', 'errors', 'warnings', 'checks', 'lines',

+ 'stdout']

+ result = collections.namedtuple('CheckPatchResult', fields)

+ result.ok = False

+ result.errors, result.warning, result.checks = 0, 0, 0

+ result.lines = 0

+ result.problems = []

+ chk = FindCheckPatch()

+ item = {}

+ result.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

+ # or:

+ # total: 0 errors, 2 warnings, 7 checks, 473 lines checked

+ re_stats = re.compile('total: (\\d+) errors, (\d+) warnings, (\d+)')

+ re_stats_full = re.compile('total: (\\d+) errors, (\d+) warnings, (\d+)'

+ ' checks, (\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_check = re.compile('CHECK: (.*)')

+ re_file = re.compile('#\d+: FILE: ([^:]*):(\d+):')

+

+ for line in result.stdout.splitlines():

+ if verbose:

+ print line

+

+ # A blank line indicates the end of a message

+ if not line and item:

+ result.problems.append(item)

+ item = {}

+ match = re_stats_full.match(line)

+ if not match:

+ match = re_stats.match(line)

+ if match:

+ result.errors = int(match.group(1))

+ result.warnings = int(match.group(2))

+ if len(match.groups()) == 4:

+ result.checks = int(match.group(3))

+ result.lines = int(match.group(4))

+ else:

+ result.lines = int(match.group(3))

+ elif re_ok.match(line):

+ result.ok = True

+ elif re_bad.match(line):

+ result.ok = False

+ err_match = re_error.match(line)

+ warn_match = re_warning.match(line)

+ file_match = re_file.match(line)

+ check_match = re_check.match(line)

+ if err_match:

+ item['msg'] = err_match.group(1)

+ item['type'] = 'error'

+ elif warn_match:

+ item['msg'] = warn_match.group(1)

+ item['type'] = 'warning'

+ elif check_match:

+ item['msg'] = check_match.group(1)

+ item['type'] = 'check'

+ elif file_match:

+ item['file'] = file_match.group(1)

+ item['line'] = int(file_match.group(2))

+

+ return result

+

+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)

+ elif msg_type == 'check':

+ msg_type = col.Color(col.MAGENTA, 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, warning_count, check_count = 0, 0, 0

+ col = terminal.Color()

+

+ for fname in args:

+ result = CheckPatch(fname, verbose)

+ if not result.ok:

+ error_count += result.errors

+ warning_count += result.warnings

+ check_count += result.checks

+ print '%d errors, %d warnings, %d checks for %s:' % (result.errors,

+ result.warnings, result.checks, col.Color(col.BLUE, fname))

+ if (len(result.problems) != result.errors + result.warnings +

+ result.checks):

+ print "Internal error: some problems lost"

+ for item in result.problems:

+ print GetWarningMsg(col, item.get('type', '<unknown>'),

+ item.get('file', '<unknown>'),

+ item.get('line', 0), item.get('msg', 'message'))

+ print

+ #print stdout

+ if error_count or warning_count or check_count:

+ str = 'checkpatch.pl found %d error(s), %d warning(s), %d checks(s)'

+ color = col.GREEN

+ if warning_count:

+ color = col.YELLOW

+ if error_count:

+ color = col.RED

+ print col.Color(color, str % (error_count, warning_count, check_count))

+ return False

+ return True

diff --git a/tools/patman/command.py b/tools/patman/command.py

new file mode 100644

index 0000000..d586f11

--- /dev/null

+++ b/tools/patman/command.py

@@ -0,0 +1,123 @@

+# Copyright (c) 2011 The Chromium OS Authors.

+#

+# SPDX-License-Identifier: GPL-2.0+

+#

+

+import os

+import cros_subprocess

+

+"""Shell command ease-ups for Python."""

+

+class CommandResult:

+ """A class which captures the result of executing a command.

+

+ Members:

+ stdout: stdout obtained from command, as a string

+ stderr: stderr obtained from command, as a string

+ return_code: Return code from command

+ exception: Exception received, or None if all ok

+ """

+ def __init__(self):

+ self.stdout = None

+ self.stderr = None

+ self.combined = None

+ self.return_code = None

+ self.exception = None

+

+ def __init__(self, stdout='', stderr='', combined='', return_code=0,

+ exception=None):

+ self.stdout = stdout

+ self.stderr = stderr

+ self.combined = combined

+ self.return_code = return_code

+ self.exception = exception

+

+

+# This permits interception of RunPipe for test purposes. If it is set to

+# a function, then that function is called with the pipe list being

+# executed. Otherwise, it is assumed to be a CommandResult object, and is

+# returned as the result for every RunPipe() call.

+# When this value is None, commands are executed as normal.

+test_result = None

+

+def RunPipe(pipe_list, infile=None, outfile=None,

+ capture=False, capture_stderr=False, oneline=False,

+ raise_on_error=True, cwd=None, **kwargs):

+ """

+ Perform a command pipeline, with optional input/output filenames.

+

+ Args:

+ pipe_list: List of command lines to execute. Each command line is

+ piped into the next, and is itself a list of strings. For

+ example [ ['ls', '.git'] ['wc'] ] will pipe the output of

+ 'ls .git' into 'wc'.

+ infile: File to provide stdin to the pipeline

+ outfile: File to store stdout

+ capture: True to capture output

+ capture_stderr: True to capture stderr

+ oneline: True to strip newline chars from output

+ kwargs: Additional keyword arguments to cros_subprocess.Popen()

+ Returns:

+ CommandResult object

+ """

+ if test_result:

+ if hasattr(test_result, '__call__'):

+ return test_result(pipe_list=pipe_list)

+ return test_result

+ result = CommandResult()

+ last_pipe = None

+ pipeline = list(pipe_list)

+ user_pipestr = '|'.join([' '.join(pipe) for pipe in pipe_list])

+ kwargs['stdout'] = None

+ kwargs['stderr'] = None

+ while pipeline:

+ cmd = pipeline.pop(0)

+ if last_pipe is not None:

+ kwargs['stdin'] = last_pipe.stdout

+ elif infile:

+ kwargs['stdin'] = open(infile, 'rb')

+ if pipeline or capture:

+ kwargs['stdout'] = cros_subprocess.PIPE

+ elif outfile:

+ kwargs['stdout'] = open(outfile, 'wb')

+ if capture_stderr:

+ kwargs['stderr'] = cros_subprocess.PIPE

+

+ try:

+ last_pipe = cros_subprocess.Popen(cmd, cwd=cwd, **kwargs)

+ except Exception, err:

+ result.exception = err

+ if raise_on_error:

+ raise Exception("Error running '%s': %s" % (user_pipestr, str))

+ result.return_code = 255

+ return result

+

+ if capture:

+ result.stdout, result.stderr, result.combined = (

+ last_pipe.CommunicateFilter(None))

+ if result.stdout and oneline:

+ result.output = result.stdout.rstrip('\r\n')

+ result.return_code = last_pipe.wait()

+ else:

+ result.return_code = os.waitpid(last_pipe.pid, 0)[1]

+ if raise_on_error and result.return_code:

+ raise Exception("Error running '%s'" % user_pipestr)

+ return result

+

+def Output(*cmd):

+ return RunPipe([cmd], capture=True, raise_on_error=False).stdout

+

+def OutputOneLine(*cmd, **kwargs):

+ raise_on_error = kwargs.pop('raise_on_error', True)

+ return (RunPipe([cmd], capture=True, oneline=True,

+ raise_on_error=raise_on_error,

+ **kwargs).stdout.strip())

+

+def Run(*cmd, **kwargs):

+ return RunPipe([cmd], **kwargs).stdout

+

+def RunList(cmd):

+ return RunPipe([cmd], capture=True).stdout

+

+def StopAll():

+ cros_subprocess.stay_alive = False

diff --git a/tools/patman/commit.py b/tools/patman/commit.py

new file mode 100644

index 0000000..3e0adb8

--- /dev/null

+++ b/tools/patman/commit.py

@@ -0,0 +1,88 @@

+# Copyright (c) 2011 The Chromium OS Authors.

+#

+# SPDX-License-Identifier: GPL-2.0+

+#

+

+import re

+

+# Separates a tag: at the beginning of the subject from the rest of it

+re_subject_tag = re.compile('([^:\s]*):\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)

+ cc_list: List of people to aliases/emails to cc on this commit

+ notes: List of lines in the commit (not series) notes

+ """

+ def __init__(self, hash):

+ self.hash = hash

+ self.subject = None

+ self.tags = []

+ self.changes = {}

+ self.cc_list = []

+ self.signoff_set = set()

+ self.notes = []

+

+ 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: fort: Change the widget to propound correctly

+

+ Here the tags are propounder and fort. 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

+

+ def AddCc(self, cc_list):

+ """Add a list of people to Cc when we send this patch.

+

+ Args:

+ cc_list: List of aliases or email addresses

+ """

+ self.cc_list += cc_list

+

+ def CheckDuplicateSignoff(self, signoff):

+ """Check a list of signoffs we have send for this patch

+

+ Args:

+ signoff: Signoff line

+ Returns:

+ True if this signoff is new, False if we have already seen it.

+ """

+ if signoff in self.signoff_set:

+ return False

+ self.signoff_set.add(signoff)

+ return True

diff --git a/tools/patman/cros_subprocess.py b/tools/patman/cros_subprocess.py

new file mode 100644

index 0000000..0fc4a06

--- /dev/null

+++ b/tools/patman/cros_subprocess.py

@@ -0,0 +1,397 @@

+# Copyright (c) 2012 The Chromium OS Authors.

+# Use of this source code is governed by a BSD-style license that can be

+# found in the LICENSE file.

+#

+# Copyright (c) 2003-2005 by Peter Astrand <astrand@lysator.liu.se>

+# Licensed to PSF under a Contributor Agreement.

+# See http://www.python.org/2.4/license for licensing details.

+

+"""Subprocress execution

+

+This module holds a subclass of subprocess.Popen with our own required

+features, mainly that we get access to the subprocess output while it

+is running rather than just at the end. This makes it easiler to show

+progress information and filter output in real time.

+"""

+

+import errno

+import os

+import pty

+import select

+import subprocess

+import sys

+import unittest

+

+

+# Import these here so the caller does not need to import subprocess also.

+PIPE = subprocess.PIPE

+STDOUT = subprocess.STDOUT

+PIPE_PTY = -3 # Pipe output through a pty

+stay_alive = True

+

+

+class Popen(subprocess.Popen):

+ """Like subprocess.Popen with ptys and incremental output

+

+ This class deals with running a child process and filtering its output on

+ both stdout and stderr while it is running. We do this so we can monitor

+ progress, and possibly relay the output to the user if requested.

+

+ The class is similar to subprocess.Popen, the equivalent is something like:

+

+ Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

+

+ But this class has many fewer features, and two enhancement:

+

+ 1. Rather than getting the output data only at the end, this class sends it

+ to a provided operation as it arrives.

+ 2. We use pseudo terminals so that the child will hopefully flush its output

+ to us as soon as it is produced, rather than waiting for the end of a

+ line.

+

+ Use CommunicateFilter() to handle output from the subprocess.

+

+ """

+

+ def __init__(self, args, stdin=None, stdout=PIPE_PTY, stderr=PIPE_PTY,

+ shell=False, cwd=None, env=None, **kwargs):

+ """Cut-down constructor

+

+ Args:

+ args: Program and arguments for subprocess to execute.

+ stdin: See subprocess.Popen()

+ stdout: See subprocess.Popen(), except that we support the sentinel

+ value of cros_subprocess.PIPE_PTY.

+ stderr: See subprocess.Popen(), except that we support the sentinel

+ value of cros_subprocess.PIPE_PTY.

+ shell: See subprocess.Popen()

+ cwd: Working directory to change to for subprocess, or None if none.

+ env: Environment to use for this subprocess, or None to inherit parent.

+ kwargs: No other arguments are supported at the moment. Passing other

+ arguments will cause a ValueError to be raised.

+ """

+ stdout_pty = None

+ stderr_pty = None

+

+ if stdout == PIPE_PTY:

+ stdout_pty = pty.openpty()

+ stdout = os.fdopen(stdout_pty[1])

+ if stderr == PIPE_PTY:

+ stderr_pty = pty.openpty()

+ stderr = os.fdopen(stderr_pty[1])

+

+ super(Popen, self).__init__(args, stdin=stdin,

+ stdout=stdout, stderr=stderr, shell=shell, cwd=cwd, env=env,

+ **kwargs)

+

+ # If we're on a PTY, we passed the slave half of the PTY to the subprocess.

+ # We want to use the master half on our end from now on. Setting this here

+ # does make some assumptions about the implementation of subprocess, but

+ # those assumptions are pretty minor.

+

+ # Note that if stderr is STDOUT, then self.stderr will be set to None by

+ # this constructor.

+ if stdout_pty is not None:

+ self.stdout = os.fdopen(stdout_pty[0])

+ if stderr_pty is not None:

+ self.stderr = os.fdopen(stderr_pty[0])

+

+ # Insist that unit tests exist for other arguments we don't support.

+ if kwargs:

+ raise ValueError("Unit tests do not test extra args - please add tests")

+

+ def CommunicateFilter(self, output):

+ """Interact with process: Read data from stdout and stderr.

+

+ This method runs until end-of-file is reached, then waits for the

+ subprocess to terminate.

+

+ The output function is sent all output from the subprocess and must be

+ defined like this:

+

+ def Output([self,] stream, data)

+ Args:

+ stream: the stream the output was received on, which will be

+ sys.stdout or sys.stderr.

+ data: a string containing the data

+

+ Note: The data read is buffered in memory, so do not use this

+ method if the data size is large or unlimited.

+

+ Args:

+ output: Function to call with each fragment of output.

+

+ Returns:

+ A tuple (stdout, stderr, combined) which is the data received on

+ stdout, stderr and the combined data (interleaved stdout and stderr).

+

+ Note that the interleaved output will only be sensible if you have

+ set both stdout and stderr to PIPE or PIPE_PTY. Even then it depends on

+ the timing of the output in the subprocess. If a subprocess flips

+ between stdout and stderr quickly in succession, by the time we come to

+ read the output from each we may see several lines in each, and will read

+ all the stdout lines, then all the stderr lines. So the interleaving

+ may not be correct. In this case you might want to pass

+ stderr=cros_subprocess.STDOUT to the constructor.

+

+ This feature is still useful for subprocesses where stderr is

+ rarely used and indicates an error.

+

+ Note also that if you set stderr to STDOUT, then stderr will be empty

+ and the combined output will just be the same as stdout.

+ """

+

+ read_set = []

+ write_set = []

+ stdout = None # Return

+ stderr = None # Return

+

+ if self.stdin:

+ # Flush stdio buffer. This might block, if the user has

+ # been writing to .stdin in an uncontrolled fashion.

+ self.stdin.flush()

+ if input:

+ write_set.append(self.stdin)

+ else:

+ self.stdin.close()

+ if self.stdout:

+ read_set.append(self.stdout)

+ stdout = []

+ if self.stderr and self.stderr != self.stdout:

+ read_set.append(self.stderr)

+ stderr = []

+ combined = []

+

+ input_offset = 0

+ while read_set or write_set:

+ try:

+ rlist, wlist, _ = select.select(read_set, write_set, [], 0.2)

+ except select.error, e:

+ if e.args[0] == errno.EINTR:

+ continue

+ raise

+

+ if not stay_alive:

+ self.terminate()

+

+ if self.stdin in wlist:

+ # When select has indicated that the file is writable,

+ # we can write up to PIPE_BUF bytes without risk

+ # blocking. POSIX defines PIPE_BUF >= 512

+ chunk = input[input_offset : input_offset + 512]

+ bytes_written = os.write(self.stdin.fileno(), chunk)

+ input_offset += bytes_written

+ if input_offset >= len(input):

+ self.stdin.close()

+ write_set.remove(self.stdin)

+

+ if self.stdout in rlist:

+ data = ""

+ # We will get an error on read if the pty is closed

+ try:

+ data = os.read(self.stdout.fileno(), 1024)

+ except OSError:

+ pass

+ if data == "":

+ self.stdout.close()

+ read_set.remove(self.stdout)

+ else:

+ stdout.append(data)

+ combined.append(data)

+ if output:

+ output(sys.stdout, data)

+ if self.stderr in rlist:

+ data = ""

+ # We will get an error on read if the pty is closed

+ try:

+ data = os.read(self.stderr.fileno(), 1024)

+ except OSError:

+ pass

+ if data == "":

+ self.stderr.close()

+ read_set.remove(self.stderr)

+ else:

+ stderr.append(data)

+ combined.append(data)

+ if output:

+ output(sys.stderr, data)

+

+ # All data exchanged. Translate lists into strings.

+ if stdout is not None:

+ stdout = ''.join(stdout)

+ else:

+ stdout = ''

+ if stderr is not None:

+ stderr = ''.join(stderr)

+ else:

+ stderr = ''

+ combined = ''.join(combined)

+

+ # Translate newlines, if requested. We cannot let the file

+ # object do the translation: It is based on stdio, which is

+ # impossible to combine with select (unless forcing no

+ # buffering).

+ if self.universal_newlines and hasattr(file, 'newlines'):

+ if stdout:

+ stdout = self._translate_newlines(stdout)

+ if stderr:

+ stderr = self._translate_newlines(stderr)

+

+ self.wait()

+ return (stdout, stderr, combined)

+

+

+# Just being a unittest.TestCase gives us 14 public methods. Unless we

+# disable this, we can only have 6 tests in a TestCase. That's not enough.

+#

+# pylint: disable=R0904

+

+class TestSubprocess(unittest.TestCase):

+ """Our simple unit test for this module"""

+

+ class MyOperation:

+ """Provides a operation that we can pass to Popen"""

+ def __init__(self, input_to_send=None):

+ """Constructor to set up the operation and possible input.

+

+ Args:

+ input_to_send: a text string to send when we first get input. We will

+ add \r\n to the string.

+ """

+ self.stdout_data = ''

+ self.stderr_data = ''

+ self.combined_data = ''

+ self.stdin_pipe = None

+ self._input_to_send = input_to_send

+ if input_to_send:

+ pipe = os.pipe()

+ self.stdin_read_pipe = pipe[0]

+ self._stdin_write_pipe = os.fdopen(pipe[1], 'w')

+

+ def Output(self, stream, data):

+ """Output handler for Popen. Stores the data for later comparison"""

+ if stream == sys.stdout:

+ self.stdout_data += data

+ if stream == sys.stderr:

+ self.stderr_data += data

+ self.combined_data += data

+

+ # Output the input string if we have one.

+ if self._input_to_send:

+ self._stdin_write_pipe.write(self._input_to_send + '\r\n')

+ self._stdin_write_pipe.flush()

+

+ def _BasicCheck(self, plist, oper):

+ """Basic checks that the output looks sane."""

+ self.assertEqual(plist[0], oper.stdout_data)

+ self.assertEqual(plist[1], oper.stderr_data)

+ self.assertEqual(plist[2], oper.combined_data)

+

+ # The total length of stdout and stderr should equal the combined length

+ self.assertEqual(len(plist[0]) + len(plist[1]), len(plist[2]))

+

+ def test_simple(self):

+ """Simple redirection: Get process list"""

+ oper = TestSubprocess.MyOperation()

+ plist = Popen(['ps']).CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+

+ def test_stderr(self):

+ """Check stdout and stderr"""

+ oper = TestSubprocess.MyOperation()

+ cmd = 'echo fred >/dev/stderr && false || echo bad'

+ plist = Popen([cmd], shell=True).CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+ self.assertEqual(plist [0], 'bad\r\n')

+ self.assertEqual(plist [1], 'fred\r\n')

+

+ def test_shell(self):

+ """Check with and without shell works"""

+ oper = TestSubprocess.MyOperation()

+ cmd = 'echo test >/dev/stderr'

+ self.assertRaises(OSError, Popen, [cmd], shell=False)

+ plist = Popen([cmd], shell=True).CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+ self.assertEqual(len(plist [0]), 0)

+ self.assertEqual(plist [1], 'test\r\n')

+

+ def test_list_args(self):

+ """Check with and without shell works using list arguments"""

+ oper = TestSubprocess.MyOperation()

+ cmd = ['echo', 'test', '>/dev/stderr']

+ plist = Popen(cmd, shell=False).CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+ self.assertEqual(plist [0], ' '.join(cmd[1:]) + '\r\n')

+ self.assertEqual(len(plist [1]), 0)

+

+ oper = TestSubprocess.MyOperation()

+

+ # this should be interpreted as 'echo' with the other args dropped

+ cmd = ['echo', 'test', '>/dev/stderr']

+ plist = Popen(cmd, shell=True).CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+ self.assertEqual(plist [0], '\r\n')

+

+ def test_cwd(self):

+ """Check we can change directory"""

+ for shell in (False, True):

+ oper = TestSubprocess.MyOperation()

+ plist = Popen('pwd', shell=shell, cwd='/tmp').CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+ self.assertEqual(plist [0], '/tmp\r\n')

+

+ def test_env(self):

+ """Check we can change environment"""

+ for add in (False, True):

+ oper = TestSubprocess.MyOperation()

+ env = os.environ

+ if add:

+ env ['FRED'] = 'fred'

+ cmd = 'echo $FRED'

+ plist = Popen(cmd, shell=True, env=env).CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+ self.assertEqual(plist [0], add and 'fred\r\n' or '\r\n')

+

+ def test_extra_args(self):

+ """Check we can't add extra arguments"""

+ self.assertRaises(ValueError, Popen, 'true', close_fds=False)

+

+ def test_basic_input(self):

+ """Check that incremental input works

+

+ We set up a subprocess which will prompt for name. When we see this prompt

+ we send the name as input to the process. It should then print the name

+ properly to stdout.

+ """

+ oper = TestSubprocess.MyOperation('Flash')

+ prompt = 'What is your name?: '

+ cmd = 'echo -n "%s"; read name; echo Hello $name' % prompt

+ plist = Popen([cmd], stdin=oper.stdin_read_pipe,

+ shell=True).CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+ self.assertEqual(len(plist [1]), 0)

+ self.assertEqual(plist [0], prompt + 'Hello Flash\r\r\n')

+

+ def test_isatty(self):

+ """Check that ptys appear as terminals to the subprocess"""

+ oper = TestSubprocess.MyOperation()

+ cmd = ('if [ -t %d ]; then echo "terminal %d" >&%d; '

+ 'else echo "not %d" >&%d; fi;')

+ both_cmds = ''

+ for fd in (1, 2):

+ both_cmds += cmd % (fd, fd, fd, fd, fd)

+ plist = Popen(both_cmds, shell=True).CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+ self.assertEqual(plist [0], 'terminal 1\r\n')

+ self.assertEqual(plist [1], 'terminal 2\r\n')

+

+ # Now try with PIPE and make sure it is not a terminal

+ oper = TestSubprocess.MyOperation()

+ plist = Popen(both_cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE,

+ shell=True).CommunicateFilter(oper.Output)

+ self._BasicCheck(plist, oper)

+ self.assertEqual(plist [0], 'not 1\n')

+ self.assertEqual(plist [1], 'not 2\n')

+

+if __name__ == '__main__':

+ unittest.main()

diff --git a/tools/patman/get_maintainer.py b/tools/patman/get_maintainer.py

new file mode 100644

index 0000000..00b4939

--- /dev/null

+++ b/tools/patman/get_maintainer.py

@@ -0,0 +1,47 @@

+# Copyright (c) 2012 The Chromium OS Authors.

+#

+# SPDX-License-Identifier: GPL-2.0+

+#

+

+import command

+import gitutil

+import os

+

+def FindGetMaintainer():

+ """Look for the get_maintainer.pl script.

+

+ Returns:

+ If the script is found we'll return a path to it; else None.

+ """

+ try_list = [

+ os.path.join(gitutil.GetTopLevel(), 'scripts'),

+ ]

+ # Look in the list

+ for path in try_list:

+ fname = os.path.join(path, 'get_maintainer.pl')

+ if os.path.isfile(fname):

+ return fname

+

+ return None

+

+def GetMaintainer(fname, verbose=False):

+ """Run get_maintainer.pl on a file if we find it.

+

+ We look for get_maintainer.pl in the 'scripts' directory at the top of

+ git. If we find it we'll run it. If we don't find get_maintainer.pl

+ then we fail silently.

+

+ Args:

+ fname: Path to the patch file to run get_maintainer.pl on.

+

+ Returns:

+ A list of email addresses to CC to.

+ """

+ get_maintainer = FindGetMaintainer()

+ if not get_maintainer:

+ if verbose:

+ print "WARNING: Couldn't find get_maintainer.pl"

+ return []

+

+ stdout = command.Output(get_maintainer, '--norolestats', fname)

+ return stdout.splitlines()

diff --git a/tools/patman/gitutil.py b/tools/patman/gitutil.py

new file mode 100644

index 0000000..9e739d8

--- /dev/null

+++ b/tools/patman/gitutil.py

@@ -0,0 +1,582 @@

+# Copyright (c) 2011 The Chromium OS Authors.

+#

+# SPDX-License-Identifier: GPL-2.0+

+#

+

+import command

+import re

+import os

+import series

+import subprocess

+import sys

+import terminal

+

+import checkpatch

+import settings

+

+# True to use --no-decorate - we check this in Setup()

+use_no_decorate = True

+

+def LogCmd(commit_range, git_dir=None, oneline=False, reverse=False,

+ count=None):

+ """Create a command to perform a 'git log'

+

+ Args:

+ commit_range: Range expression to use for log, None for none

+ git_dir: Path to git repositiory (None to use default)

+ oneline: True to use --oneline, else False

+ reverse: True to reverse the log (--reverse)

+ count: Number of commits to list, or None for no limit

+ Return:

+ List containing command and arguments to run

+ """

+ cmd = ['git']

+ if git_dir:

+ cmd += ['--git-dir', git_dir]

+ cmd += ['--no-pager', 'log', '--no-color']

+ if oneline:

+ cmd.append('--oneline')

+ if use_no_decorate:

+ cmd.append('--no-decorate')

+ if reverse:

+ cmd.append('--reverse')

+ if count is not None:

+ cmd.append('-n%d' % count)

+ if commit_range:

+ cmd.append(commit_range)

+ return cmd

+

+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.

+

+ Return:

+ Number of patches that exist on top of the branch

+ """

+ pipe = [LogCmd('@{upstream}..', oneline=True),

+ ['wc', '-l']]

+ stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout

+ patch_count = int(stdout)

+ return patch_count

+

+def NameRevision(commit_hash):

+ """Gets the revision name for a commit

+

+ Args:

+ commit_hash: Commit hash to look up

+

+ Return:

+ Name of revision, if any, else None

+ """

+ pipe = ['git', 'name-rev', commit_hash]

+ stdout = command.RunPipe([pipe], capture=True, oneline=True).stdout

+

+ # We expect a commit, a space, then a revision name

+ name = stdout.split(' ')[1].strip()

+ return name

+

+def GuessUpstream(git_dir, branch):

+ """Tries to guess the upstream for a branch

+

+ This lists out top commits on a branch and tries to find a suitable

+ upstream. It does this by looking for the first commit where

+ 'git name-rev' returns a plain branch name, with no ! or ^ modifiers.

+

+ Args:

+ git_dir: Git directory containing repo

+ branch: Name of branch

+

+ Returns:

+ Tuple:

+ Name of upstream branch (e.g. 'upstream/master') or None if none

+ Warning/error message, or None if none

+ """

+ pipe = [LogCmd(branch, git_dir=git_dir, oneline=True, count=100)]

+ result = command.RunPipe(pipe, capture=True, capture_stderr=True,

+ raise_on_error=False)

+ if result.return_code:

+ return None, "Branch '%s' not found" % branch

+ for line in result.stdout.splitlines()[1:]:

+ commit_hash = line.split(' ')[0]

+ name = NameRevision(commit_hash)

+ if '~' not in name and '^' not in name:

+ if name.startswith('remotes/'):

+ name = name[8:]

+ return name, "Guessing upstream as '%s'" % name

+ return None, "Cannot find a suitable upstream for branch '%s'" % branch

+

+def GetUpstream(git_dir, branch):

+ """Returns the name of the upstream for a branch

+

+ Args:

+ git_dir: Git directory containing repo

+ branch: Name of branch

+

+ Returns:

+ Tuple:

+ Name of upstream branch (e.g. 'upstream/master') or None if none

+ Warning/error message, or None if none

+ """

+ try:

+ remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',

+ 'branch.%s.remote' % branch)

+ merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',

+ 'branch.%s.merge' % branch)

+ except:

+ upstream, msg = GuessUpstream(git_dir, branch)

+ return upstream, msg

+

+ if remote == '.':

+ return merge, None

+ elif remote and merge:

+ leaf = merge.split('/')[-1]

+ return '%s/%s' % (remote, leaf), None

+ else:

+ raise ValueError, ("Cannot determine upstream branch for branch "

+ "'%s' remote='%s', merge='%s'" % (branch, remote, merge))

+

+

+def GetRangeInBranch(git_dir, branch, include_upstream=False):

+ """Returns an expression for the commits in the given branch.

+

+ Args:

+ git_dir: Directory containing git repo

+ branch: Name of branch

+ Return:

+ Expression in the form 'upstream..branch' which can be used to

+ access the commits. If the branch does not exist, returns None.

+ """

+ upstream, msg = GetUpstream(git_dir, branch)

+ if not upstream:

+ return None, msg

+ rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)

+ return rstr, msg

+

+def CountCommitsInRange(git_dir, range_expr):

+ """Returns the number of commits in the given range.

+

+ Args:

+ git_dir: Directory containing git repo

+ range_expr: Range to check

+ Return:

+ Number of patches that exist in the supplied rangem or None if none

+ were found

+ """

+ pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True)]

+ result = command.RunPipe(pipe, capture=True, capture_stderr=True,

+ raise_on_error=False)

+ if result.return_code:

+ return None, "Range '%s' not found or is invalid" % range_expr

+ patch_count = len(result.stdout.splitlines())

+ return patch_count, None

+

+def CountCommitsInBranch(git_dir, branch, include_upstream=False):

+ """Returns the number of commits in the given branch.

+

+ Args:

+ git_dir: Directory containing git repo

+ branch: Name of branch

+ Return:

+ Number of patches that exist on top of the branch, or None if the

+ branch does not exist.

+ """

+ range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream)

+ if not range_expr:

+ return None, msg

+ return CountCommitsInRange(git_dir, range_expr)

+

+def CountCommits(commit_range):

+ """Returns the number of commits in the given range.

+

+ Args:

+ commit_range: Range of commits to count (e.g. 'HEAD..base')

+ Return:

+ Number of patches that exist on top of the branch

+ """

+ pipe = [LogCmd(commit_range, oneline=True),

+ ['wc', '-l']]

+ stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout

+ patch_count = int(stdout)

+ return patch_count

+

+def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):

+ """Checkout the selected commit for this build

+

+ Args:

+ commit_hash: Commit hash to check out

+ """

+ pipe = ['git']

+ if git_dir:

+ pipe.extend(['--git-dir', git_dir])

+ if work_tree:

+ pipe.extend(['--work-tree', work_tree])

+ pipe.append('checkout')

+ if force:

+ pipe.append('-f')

+ pipe.append(commit_hash)

+ result = command.RunPipe([pipe], capture=True, raise_on_error=False,

+ capture_stderr=True)

+ if result.return_code != 0:

+ raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)

+

+def Clone(git_dir, output_dir):

+ """Checkout the selected commit for this build

+

+ Args:

+ commit_hash: Commit hash to check out

+ """

+ pipe = ['git', 'clone', git_dir, '.']

+ result = command.RunPipe([pipe], capture=True, cwd=output_dir,

+ capture_stderr=True)

+ if result.return_code != 0:

+ raise OSError, 'git clone: %s' % result.stderr

+

+def Fetch(git_dir=None, work_tree=None):

+ """Fetch from the origin repo

+

+ Args:

+ commit_hash: Commit hash to check out

+ """

+ pipe = ['git']

+ if git_dir:

+ pipe.extend(['--git-dir', git_dir])

+ if work_tree:

+ pipe.extend(['--work-tree', work_tree])

+ pipe.append('fetch')

+ result = command.RunPipe([pipe], capture=True, capture_stderr=True)

+ if result.return_code != 0:

+ raise OSError, 'git fetch: %s' % result.stderr

+

+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', '-M', '--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 BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):

+ """Build a list of email addresses based on an input list.

+

+ Takes a list of email addresses and aliases, and turns this into a list

+ of only email address, by resolving any aliases that are present.

+

+ If the tag is given, then each email address is prepended with this

+ tag and a space. If the tag starts with a minus sign (indicating a

+ command line parameter) then the email address is quoted.

+

+ Args:

+ in_list: List of aliases/email addresses

+ tag: Text to put before each address

+ alias: Alias dictionary

+ raise_on_error: True to raise an error when an alias fails to match,

+ False to just print a message.

+

+ Returns:

+ List of email addresses

+

+ >>> alias = {}

+ >>> alias['fred'] = ['f.bloggs@napier.co.nz']

+ >>> alias['john'] = ['j.bloggs@napier.co.nz']

+ >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']

+ >>> alias['boys'] = ['fred', ' john']

+ >>> alias['all'] = ['fred ', 'john', ' mary ']

+ >>> BuildEmailList(['john', 'mary'], None, alias)

+ ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']

+ >>> BuildEmailList(['john', 'mary'], '--to', alias)

+ ['--to "j.bloggs@napier.co.nz"', \

+'--to "Mary Poppins <m.poppins@cloud.net>"']

+ >>> BuildEmailList(['john', 'mary'], 'Cc', alias)

+ ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']

+ """

+ quote = '"' if tag and tag[0] == '-' else ''

+ raw = []

+ for item in in_list:

+ raw += LookupEmail(item, alias, raise_on_error=raise_on_error)

+ result = []

+ for item in raw:

+ if not item in result:

+ result.append(item)

+ if tag:

+ return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]

+ return result

+

+def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,

+ self_only=False, alias=None, in_reply_to=None):

+ """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

+ raise_on_error: True to raise an error when an alias fails to match,

+ False to just print a message.

+ cc_fname: Filename of Cc file for per-commit Cc

+ self_only: True to just email to yourself as a test

+ in_reply_to: If set we'll pass this to git as --in-reply-to.

+ Should be a message ID that this is in reply to.

+

+ Returns:

+ Git command that was/would be run

+

+ # For the duration of this doctest pretend that we ran patman with ./patman

+ >>> _old_argv0 = sys.argv[0]

+ >>> sys.argv[0] = './patman'

+

+ >>> alias = {}

+ >>> alias['fred'] = ['f.bloggs@napier.co.nz']

+ >>> alias['john'] = ['j.bloggs@napier.co.nz']

+ >>> alias['mary'] = ['m.poppins@cloud.net']

+ >>> alias['boys'] = ['fred', ' john']

+ >>> alias['all'] = ['fred ', 'john', ' mary ']

+ >>> alias[os.getenv('USER')] = ['this-is-me@me.com']

+ >>> series = series.Series()

+ >>> series.to = ['fred']

+ >>> series.cc = ['mary']

+ >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \

+ False, alias)

+ 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \

+"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'

+ >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \

+ alias)

+ 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \

+"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'

+ >>> series.cc = ['all']

+ >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \

+ True, alias)

+ 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \

+--cc-cmd cc-fname" cover p1 p2'

+ >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \

+ False, alias)

+ 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \

+"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \

+"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'

+

+ # Restore argv[0] since we clobbered it.

+ >>> sys.argv[0] = _old_argv0

+ """

+ to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)

+ if not to:

+ git_config_to = command.Output('git', 'config', 'sendemail.to')

+ if not git_config_to:

+ print ("No recipient.\n"

+ "Please add something like this to a commit\n"

+ "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"

+ "Or do something like this\n"

+ "git config sendemail.to u-boot@lists.denx.de")

+ return

+ cc = BuildEmailList(list(set(series.get('cc')) - set(series.get('to'))),

+ '--cc', alias, raise_on_error)

+ if self_only:

+ to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)

+ cc = []

+ cmd = ['git', 'send-email', '--annotate']

+ if in_reply_to:

+ cmd.append('--in-reply-to="%s"' % in_reply_to)

+

+ cmd += 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

+

+

+def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):

+ """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: Alias or email address to look up

+ alias: Dictionary containing aliases (None to use settings default)

+ raise_on_error: True to raise an error when an alias fails to match,

+ False to just print a message.

+

+ Returns:

+ tuple:

+ list containing a list of email addresses

+

+ Raises:

+ OSError if a recursive alias reference was found

+ ValueError if an alias was not found

+

+ >>> alias = {}

+ >>> alias['fred'] = ['f.bloggs@napier.co.nz']

+ >>> alias['john'] = ['j.bloggs@napier.co.nz']

+ >>> alias['mary'] = ['m.poppins@cloud.net']

+ >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']

+ >>> alias['all'] = ['fred ', 'john', ' mary ']

+ >>> alias['loop'] = ['other', 'john', ' mary ']

+ >>> alias['other'] = ['loop', 'john', ' mary ']

+ >>> LookupEmail('mary', alias)

+ ['m.poppins@cloud.net']

+ >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)

+ ['arthur.wellesley@howe.ro.uk']

+ >>> LookupEmail('boys', alias)

+ ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']

+ >>> LookupEmail('all', alias)

+ ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']

+ >>> LookupEmail('odd', alias)

+ Traceback (most recent call last):

+ ...

+ ValueError: Alias 'odd' not found

+ >>> LookupEmail('loop', alias)

+ Traceback (most recent call last):

+ ...

+ OSError: Recursive email alias at 'other'

+ >>> LookupEmail('odd', alias, raise_on_error=False)

+ Alias 'odd' not found

+ []

+ >>> # In this case the loop part will effectively be ignored.

+ >>> LookupEmail('loop', alias, raise_on_error=False)

+ Recursive email alias at 'other'

+ Recursive email alias at 'john'

+ Recursive email alias at 'mary'

+ ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']

+ """

+ if not alias:

+ alias = settings.alias

+ lookup_name = lookup_name.strip()

+ if '@' in lookup_name: # Perhaps a real email address

+ return [lookup_name]

+

+ lookup_name = lookup_name.lower()

+ col = terminal.Color()

+

+ out_list = []

+ if level > 10:

+ msg = "Recursive email alias at '%s'" % lookup_name

+ if raise_on_error:

+ raise OSError, msg

+ else:

+ print col.Color(col.RED, msg)

+ return out_list

+

+ if lookup_name:

+ if not lookup_name in alias:

+ msg = "Alias '%s' not found" % lookup_name

+ if raise_on_error:

+ raise ValueError, msg

+ else:

+ print col.Color(col.RED, msg)

+ return out_list

+ for item in alias[lookup_name]:

+ todo = LookupEmail(item, alias, raise_on_error, level + 1)

+ for new_item in todo:

+ if not new_item in out_list:

+ out_list.append(new_item)

+

+ #print "No match for alias '%s'" % lookup_name

+ return out_list

+

+def GetTopLevel():

+ """Return name of top-level directory for this git repo.

+

+ Returns:

+ Full path to git top-level directory

+

+ This test makes sure that we are running tests in the right subdir

+

+ >>> os.path.realpath(os.path.dirname(__file__)) == \

+ os.path.join(GetTopLevel(), 'tools', 'patman')

+ True

+ """

+ return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')

+

+def GetAliasFile():

+ """Gets the name of the git alias file.

+

+ Returns:

+ Filename of git alias file, or None if none

+ """

+ fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',

+ raise_on_error=False)

+ if fname:

+ fname = os.path.join(GetTopLevel(), fname.strip())

+ return fname

+

+def GetDefaultUserName():

+ """Gets the user.name from .gitconfig file.

+

+ Returns:

+ User name found in .gitconfig file, or None if none

+ """

+ uname = command.OutputOneLine('git', 'config', '--global', 'user.name')

+ return uname

+

+def GetDefaultUserEmail():

+ """Gets the user.email from the global .gitconfig file.

+

+ Returns:

+ User's email found in .gitconfig file, or None if none

+ """

+ uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')

+ return uemail

+

+def GetDefaultSubjectPrefix():

+ """Gets the format.subjectprefix from local .git/config file.

+

+ Returns:

+ Subject prefix found in local .git/config file, or None if none

+ """

+ sub_prefix = command.OutputOneLine('git', 'config', 'format.subjectprefix',

+ raise_on_error=False)

+

+ return sub_prefix

+

+def Setup():

+ """Set up git utils, by reading the alias files."""

+ # Check for a git alias file also

+ global use_no_decorate

+

+ alias_fname = GetAliasFile()

+ if alias_fname:

+ settings.ReadGitAliases(alias_fname)

+ cmd = LogCmd(None, count=0)

+ use_no_decorate = (command.RunPipe([cmd], raise_on_error=False)

+ .return_code == 0)

+

+def GetHead():

+ """Get the hash of the current HEAD

+

+ Returns:

+ Hash of HEAD

+ """

+ return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')

+

+if __name__ == "__main__":

+ import doctest

+

+ doctest.testmod()

diff --git a/tools/patman/patchstream.py b/tools/patman/patchstream.py

new file mode 100644

index 0000000..6d3c41f

--- /dev/null

+++ b/tools/patman/patchstream.py

@@ -0,0 +1,488 @@

+# Copyright (c) 2011 The Chromium OS Authors.

+#

+# SPDX-License-Identifier: GPL-2.0+

+#

+

+import math

+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=|^BRANCH=|^Change-Id:|^Review URL:'

+ '|Reviewed-on:|Commit-\w*:')

+

+# Lines which are allowed after a TEST= line

+re_allowed_after_test = re.compile('^Signed-off-by:')

+

+# Signoffs

+re_signoff = re.compile('^Signed-off-by: *(.*)')

+

+# The start of the cover letter

+re_cover = re.compile('^Cover-letter:')

+

+# A cover letter Cc

+re_cover_cc = re.compile('^Cover-letter-cc: *(.*)')

+

+# Patch series tag

+re_series_tag = re.compile('^Series-([a-z-]*): *(.*)')

+

+# Commit series tag

+re_commit_tag = re.compile('^Commit-([a-z-]*): *(.*)')

+

+# Commit tags that we want to collect and keep

+re_tag = re.compile('^(Tested-by|Acked-by|Reviewed-by|Patch-cc): (.*)')

+

+# The start of a new commit in the git log

+re_commit = re.compile('^commit ([0-9a-f]*)$')

+

+# 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.signoff = [] # 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(self.commit, line, name, value)

+

+ def AddToCommit(self, line, name, value):

+ """Add a new Commit-xxx tag.

+

+ When a Commit-xxx tag is detected, we come here to record it.

+

+ Args:

+ line: Source line containing tag (useful for

Show more