diff options
Diffstat (limited to 'tools/patman/gitutil.py')
| -rw-r--r-- | tools/patman/gitutil.py | 201 | 
1 files changed, 179 insertions, 22 deletions
| diff --git a/tools/patman/gitutil.py b/tools/patman/gitutil.py index ca3ba4a03..f48575013 100644 --- a/tools/patman/gitutil.py +++ b/tools/patman/gitutil.py @@ -23,11 +23,12 @@ import command  import re  import os  import series -import settings  import subprocess  import sys  import terminal +import settings +  def CountCommitsToBranch():      """Returns number of commits between HEAD and the tracking branch. @@ -40,10 +41,123 @@ def CountCommitsToBranch():      """      pipe = [['git', 'log', '--no-color', '--oneline', '@{upstream}..'],              ['wc', '-l']] -    stdout = command.RunPipe(pipe, capture=True, oneline=True) +    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout      patch_count = int(stdout)      return patch_count +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: +        Name of upstream branch (e.g. 'upstream/master') or None if none +    """ +    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) +    if remote == '.': +        return merge +    elif remote and merge: +        leaf = merge.split('/')[-1] +        return '%s/%s' % (remote, leaf) +    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. +    """ +    upstream = GetUpstream(git_dir, branch) +    return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch) + +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 +    """ +    range_expr = GetRangeInBranch(git_dir, branch, include_upstream) +    pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', range_expr], +            ['wc', '-l']] +    result = command.RunPipe(pipe, capture=True, oneline=True) +    patch_count = int(result.stdout) +    return patch_count + +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 = [['git', 'log', '--oneline', commit_range], +            ['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) +    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) +    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) +    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. @@ -159,7 +273,7 @@ def ApplyPatches(verbose, args, start_point):          print stdout, stderr      return error_count == 0 -def BuildEmailList(in_list, tag=None, alias=None): +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 @@ -172,6 +286,9 @@ def BuildEmailList(in_list, tag=None, alias=None):      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 @@ -193,7 +310,7 @@ def BuildEmailList(in_list, tag=None, alias=None):      quote = '"' if tag and tag[0] == '-' else ''      raw = []      for item in in_list: -        raw += LookupEmail(item, alias) +        raw += LookupEmail(item, alias, raise_on_error=raise_on_error)      result = []      for item in raw:          if not item in result: @@ -202,8 +319,8 @@ def BuildEmailList(in_list, tag=None, alias=None):          return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]      return result -def EmailPatches(series, cover_fname, args, dry_run, cc_fname, -        self_only=False, alias=None): +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: @@ -211,8 +328,12 @@ def EmailPatches(series, cover_fname, args, dry_run, cc_fname,          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 @@ -231,20 +352,21 @@ def EmailPatches(series, cover_fname, args, dry_run, cc_fname,      >>> series = series.Series()      >>> series.to = ['fred']      >>> series.cc = ['mary'] -    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \ -            alias) +    >>> 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, 'cc-fname', False, alias) +    >>> 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, 'cc-fname', True, \ -            alias) +    >>> 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, 'cc-fname', False, \ -            alias) +    >>> 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' @@ -252,16 +374,19 @@ def EmailPatches(series, cover_fname, args, dry_run, cc_fname,      # Restore argv[0] since we clobbered it.      >>> sys.argv[0] = _old_argv0      """ -    to = BuildEmailList(series.get('to'), '--to', alias) +    to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)      if not to:          print ("No recipient, please add something like this to a commit\n"              "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")          return -    cc = BuildEmailList(series.get('cc'), '--cc', alias) +    cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)      if self_only: -        to = BuildEmailList([os.getenv('USER')], '--to', alias) +        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)] @@ -274,13 +399,16 @@ def EmailPatches(series, cover_fname, args, dry_run, cc_fname,      return str -def LookupEmail(lookup_name, alias=None, level=0): +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: @@ -314,6 +442,15 @@ def LookupEmail(lookup_name, alias=None, level=0):      Traceback (most recent call last):      ...      OSError: Recursive email alias at 'other' +    >>> LookupEmail('odd', alias, raise_on_error=False) +    \033[1;31mAlias 'odd' not found\033[0m +    [] +    >>> # In this case the loop part will effectively be ignored. +    >>> LookupEmail('loop', alias, raise_on_error=False) +    \033[1;31mRecursive email alias at 'other'\033[0m +    \033[1;31mRecursive email alias at 'john'\033[0m +    \033[1;31mRecursive email alias at 'mary'\033[0m +    ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']      """      if not alias:          alias = settings.alias @@ -322,16 +459,27 @@ def LookupEmail(lookup_name, alias=None, level=0):          return [lookup_name]      lookup_name = lookup_name.lower() +    col = terminal.Color() +    out_list = []      if level > 10: -        raise OSError, "Recursive email alias at '%s'" % lookup_name +        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 -    out_list = []      if lookup_name:          if not lookup_name in alias: -            raise ValueError, "Alias '%s' not found" % lookup_name +            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, level + 1) +            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) @@ -359,7 +507,8 @@ def GetAliasFile():      Returns:          Filename of git alias file, or None if none      """ -    fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile') +    fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile', +            raise_on_error=False)      if fname:          fname = os.path.join(GetTopLevel(), fname.strip())      return fname @@ -389,6 +538,14 @@ def Setup():      if alias_fname:          settings.ReadGitAliases(alias_fname) +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 |