[Asterisk-code-review] Change in repotools[master]: digium_git: Add a new module to manage Git

Matt Jordan (Code Review) asteriskteam at digium.com
Wed Apr 15 16:31:40 CDT 2015


Matt Jordan has uploaded a new change for review.

  https://gerrit.asterisk.org/132

Change subject: digium_git: Add a new module to manage Git
......................................................................

digium_git: Add a new module to manage Git

This patch adds a new module, diguim_git, that wraps up Git commands in
Python for other script usage. It provides two classes:
 * GitProgressBar - this displays a pretty progress bar while the Git
                    wrapper class manipulates repositories
 * DigiumGitRepo  - a wrapper around a repo, this provides some handy
                    accessors for creating/updating an existing repo
		    and pulling commit history. Commit history obtained
		    is generally turned into DigiumCommitMessageParser
		    objects, so that the release summary scripts and
		    other Asterisk project change logs can obtain
		    testers, reporters, JIRA issues, license numbers,
		    etc.

In addition, the patch updates the following modules:
 * digium_commits - the DigiumCommitMessageParser class has been updated
                    to use the digium_jira_user module. All name mapping
		    information has been moved there.
 * digium_jira_user - added equality/not-equality operator callbacks.
                      This allows for DigiumCommitMessageParser objects
		      to determine their own equality when used as
		      members of a collection.

Change-Id: I81f66a33f8f2ff7a7d9d0cbb82428e71370536b2
---
M digium_commits.py
A digium_git.py
M digium_jira_user.py
3 files changed, 287 insertions(+), 56 deletions(-)


  git pull ssh://gerrit.asterisk.org:29418/repotools refs/changes/32/132/2

diff --git a/digium_commits.py b/digium_commits.py
index c7745ee..e08a161 100644
--- a/digium_commits.py
+++ b/digium_commits.py
@@ -2,55 +2,13 @@
 '''
 Utilities to assist with parsing Digium formatted commit messages.
 
-Copyright (C) 2009 - 2011, Digium, Inc.
+Copyright (C) 2009 - 2015, Digium, Inc.
 Russell Bryant <russell at digium.com>
 '''
 
 import re
 
-
-NAME_MAPPINGS = {
-    'jonathan rose': 'jrose',
-    'matt jordan': 'mjordan',
-    'matthew jordan': 'mjordan',
-    'joshua colp': 'jcolp',
-    'mark michelson': 'mmichelson',
-    'shaun ruffell': 'sruffell',
-    'terry wilson': 'twilson',
-    'kinsey moore': 'kmoore',
-    'richard mudgett': 'rmudgett',
-    'brent eagles': 'beagles',
-    'david lee': 'dlee',
-    'darren sessions': 'dsessions',
-    'john bigelow': 'jbigelow',
-    'walter doekes': 'wdoekes',
-    'michael l. young': 'elguero',
-    'michael young': 'elguero',
-    'ashley sanders': 'asanders',
-    'corey farrell': 'coreyfarrell',
-    'benjamin ford': 'bford',
-    'putnopvut': 'mmichelson',
-    'corydon76': 'tilghman',
-    'north': 'jparker',
-    'qwell': 'jparker',
-    'tempest1': 'bbryant',
-    'otherwiseguy': 'twilson',
-    'drumkilla': 'russell',
-    'russellb': 'russell',
-    'citats': 'jamesgolovich',
-    'blitzrage': 'lmadsen',
-    'opticron': 'kmoore',
-    'file': 'jcolp',
-    'creslin': 'mattf',
-    'tilghman lesher': 'tilghman',
-    'olle johansson': 'oej', }
-
-
-def convert_name(name):
-    if name.lower() in NAME_MAPPINGS:
-        return NAME_MAPPINGS[name.lower()]
-    return name
-
+from digium_jira_user import AsteriskUser
 
 class DigiumCommitMessageParser:
     '''
@@ -79,8 +37,8 @@
         commit of code.  The blocked attribute will be set to True if the
         string "Blocked revisions " is found in the commit message.
         '''
-        self.message = message
-        self.author = convert_name(author)
+        self.message = unicode(message).encode('utf-8')
+        self.author = AsteriskUser(author)
         self.block = False
 
         if self.message.find("Blocked revisions ") >= 0:
@@ -138,9 +96,9 @@
         for match in re.finditer("[Tt]ested [Bb]y:?(.*)", self.message):
             testers = match.group(1).split(",")
             for t in testers:
-                t = convert_name(t.strip())
-                if t not in all_testers:
-                    all_testers.append(t)
+                user = AsteriskUser(t)
+                if user not in all_testers:
+                    all_testers.append(user)
         return all_testers
 
     def get_coders(self):
@@ -159,6 +117,9 @@
         coders = []
         tokens = self.message.split('\n')
         patches = False
+        realname = None
+        email = None
+        license = None
         for token in tokens:
             if 'patches:' in token.lower():
                 patches = True
@@ -172,14 +133,24 @@
             if name:
                 if ':' in name:
                     name.replace(':', '')
+                if '<' in name:
+                    realname = name[:name.index('<')]
+                    email = name[name.index('<'):name.index('>')]
                 if '(' in name:
-                    name = name[:name.index('(')]
-                name = name.strip()
-                c = convert_name(name)
+                    if not realname:
+                        realname = name[:name.index('(')]
+                    license = name[name.index('('):name.index(')')]
+                if not realname:
+                    realname = name
+                realname = name.strip()
+                user = AsteriskUser(name)
+                user.email = email
+                user.license = license
+
                 if len(c) == 0:
                     continue
-                if c not in coders:
-                    coders.append(c)
+                if user not in coders:
+                    coders.append(user)
         if len(coders) == 0:
             coders.append(self.author)
         return coders
@@ -195,7 +166,7 @@
         reporter = ""
         match = re.search("[Rr]eported [Bb]y:?(.*)", self.message)
         if match is not None:
-            reporter = convert_name(match.group(1))
+            reporter = AsteriskUser(match.group(1))
         return reporter
 
     def get_commit_summary(self):
@@ -221,3 +192,7 @@
             return message_lines[i].strip()
 
         return "(No Summary Available)"
+
+    def __repr__(self):
+        '''Return a string represenation of the object'''
+        return 'Author: {0}\n{1}'.format(self.author, self.message)
diff --git a/digium_git.py b/digium_git.py
new file mode 100644
index 0000000..509ba73
--- /dev/null
+++ b/digium_git.py
@@ -0,0 +1,240 @@
+#!/usr/bin/env python
+"""Asterisk project Git wrapper
+
+This module uses GitPython to provide common operations on Git repositories
+that the Asterisk project commonly requires. Note that GitPython is itself
+simply a wrapper around Git, and requires Git to be installed and in the system
+path.
+
+Copyright (C) 2015, Digium, Inc.
+Matt Jordan <mjordan at digium.com>
+"""
+
+import os
+import logging
+
+from progressbar import ProgressBar
+from digium_commits import DigiumCommitMessageParser
+from git import Repo, Remote, TagReference, RemoteProgress
+
+
+class GitProgressBar(RemoteProgress):
+    """A progress bar that maintains the state of a Git operation
+    """
+
+    def __init__(self):
+        """Constructor"""
+        super(GitProgressBar, self).__init__()
+
+        self.progress_bar = None
+        self._current_op = -1
+        self._started = False
+        self._max_count = 0
+
+    def operation_to_str(self, code):
+        """Convert a GitPython.RemoteProgress op code to string
+
+        Keyword Arguments:
+        code - The code to convert to a string
+
+        Returns:
+        A string representation of the code
+        """
+        if code == 1:
+            return "BEGIN"
+        elif code == 2:
+            return "END"
+        elif code == 3:
+            return "STAGE_MASK"
+        elif code == 4:
+            return "Counting"
+        elif code == 8:
+            return "Compressing"
+        elif code == 16:
+            return "Writing"
+        elif code == 32:
+            return "Receiving"
+        elif code == 64:
+            return "Resolving"
+        elif code == 128:
+            return "Finding source"
+        else:
+            return "Stuff and things ({0})".format(code)
+
+    def update(self, op_code, cur_count, max_count=None, message=''):
+        """Called when the remote operation updates us
+
+        Override of RemoteProgress.update
+
+        Keyword Arguments:
+        op_code   - The current Git operation
+        cur_count - The current number of objects processed
+        max_count - The max number of objects for all operations
+        message   - A message received from Git
+        """
+        re_create_bar = False
+
+        op_code &= ~GitProgressBar.STAGE_MASK
+        if op_code == 0:
+            return
+
+        if op_code != self._current_op:
+            print("{0}...".format(self.operation_to_str(op_code)))
+            self._current_op = op_code
+            re_create_bar = True
+
+        if op_code == GitProgressBar.COUNTING:
+            # COUNTING - at this point, we don't have a max value.
+            # Bail.
+            return
+
+        if not self._max_count:
+            if not max_count:
+                return
+            self._max_count = max_count
+
+        if re_create_bar:
+            self._current_op = op_code
+            self.progress_bar = ProgressBar()
+            self.progress_bar.maxval = float(max_count)
+            self.progress_bar.start()
+
+        self.progress_bar.update(float(cur_count))
+
+
+class DigiumGitRepo(object):
+    """A managed Git repo
+
+    This class wraps up a Git repo and provides some common operations on it
+    that are useful for common Asterisk operations. This includes cloning
+    and fetching the repo, providing useful logs/summaries, etc.
+    """
+
+    def __init__(self, local_path, repo_url=None, show_progress=False):
+        """Constructor
+
+        Keyword Arguments:
+        local_path    - The local location on disk to create or set the repo to.
+                        If this location already exists, a fetch will be performed
+                        from its repo. If it does not exist, the location will be
+                        created, initialized, and set to the repo passed in.
+        repo_url      - The repository to pull in. Note that if local_path does
+                        not exist, this must be specified. An exception will be
+                        thrown if local_path does not exist but no repo was
+                        provided.
+        show_progress - If True, print out a progress bar for the various
+                        operations being performed.
+        """
+
+        progress = None
+
+        if os.path.isdir(local_path):
+            self.repo = Repo(local_path)
+            origin = self.repo.remotes.origin
+        else:
+            if not repo_url:
+                raise ValueError("local_path {0} does not exist and "
+                                 "repo is None".format(local_path))
+            os.makedirs(local_path)
+            self.repo = Repo()
+            if show_progress:
+                progress = GitProgressBar()
+            self.repo = self.repo.clone_from(url=repo_url,
+                                             to_path=local_path,
+                                             progress=progress)
+            origin = self.repo.remotes.origin
+        if show_progress:
+            progress = GitProgressBar()
+        origin.fetch(progress=progress)
+        if show_progress:
+            progress = GitProgressBar()
+        origin.pull(progress=progress)
+
+    def checkout_remote_branch(self, branch_name):
+        """Set up a local tracking branch of a remote branch
+
+        Keyword Arguments:
+        branch_name - The remote branch to track
+        """
+        origin = self.repo.remotes.origin
+        remote_ref = [ref for ref in origin.refs if branch_name in ref.name][0]
+        local_branch = self.repo.create_head(branch_name, remote_ref)
+        local_branch.set_tracking_branch(remote_ref)
+        local_branch.checkout()
+
+    def get_tag(self, tag_name):
+        """Retrieve a particular tag ref from the repo
+
+        Raises an exception if tag_name is not found.
+
+        Keyword Arguments:
+        tag_name - The name of the tag to retrieve
+
+        Returns:
+        The tag object
+        """
+        tag = [t for t in self.repo.tags if t.name == tag_name][0]
+        return tag
+
+    def _convert_git_to_digium_commit(self, git_commit):
+        """Convert a Git commit into a Digium/Asterisk commit
+
+        Keyword Arguments:
+        git_commit - The Git commit object to convert
+
+        Returns:
+        A DigiumCommitMessageParser object
+        """
+
+        digium_commit = DigiumCommitMessageParser(
+                            git_commit.message,
+                            unicode(git_commit.author.name).encode('utf-8'))
+        digium_commit.author.email = unicode(git_commit.author.email).encode('utf-8')
+        return digium_commit
+
+    def _convert_git_to_digium_commits(self, git_commits):
+        """Convert a series of Git commits to Digium/Asterisk commits
+
+        Keyword Arguments:
+        git_commits - A list of Git commits to convert
+
+        Returns:
+        A DigiumCommitMessageParser object
+        """
+        digium_commits = []
+        for git_commit in git_commits:
+            digium_commit = self._convert_git_to_digium_commit(git_commit)
+            digium_commits.append(digium_commit)
+        return digium_commits
+
+    def get_commits_by_tags(self, start_tag, end_tag):
+        """Retrieve a sequence of commits between two tags
+
+        Keyword Arguments:
+        start_tag - The start tag to begin pulling history
+        end_tag   - The end tag to stop at
+
+        Returns:
+        A list of DigiumCommitMessageParser objects
+        """
+        commit_start = self.repo.commit(start_tag)
+        commit_end = self.repo.commit(end_tag)
+        return self._convert_git_to_digium_commits(
+                list(self.repo.iter_commits(rev='{0}..{1}'.format(
+                        commit_start, commit_end))))
+
+    def get_commits_by_date(self, branch, start_date, end_date):
+        """Retrieve a sequence of commits between two dates
+
+        Keyword Arguments:
+        start_date - The start date to pull history from
+        end_date   - The end date to stop at
+
+        Returns:
+        A list of DigiumCommitMessageParser objects
+        """
+        return self._convert_git_to_digium_commits(
+                list(self.repo.iter_commits(rev=branch,
+                    after=str(start_date), before=str(end_date))))
+
+
diff --git a/digium_jira_user.py b/digium_jira_user.py
index f17839d..47a69fe 100644
--- a/digium_jira_user.py
+++ b/digium_jira_user.py
@@ -59,7 +59,7 @@
         """Constructor
 
         Keyword Arguments:
-        username - The unique ID for the user
+        username  - The unique ID for the user. Optional.
         """
         self.full_name = None
         self._username = None
@@ -91,6 +91,22 @@
                 self.organization = organization
                 break
 
+    def __eq__(self, other):
+        """Test for equality
+
+        Keyword Arguments:
+        other - The other item being compared against this one
+        """
+        return self.username == other.username
+
+    def __ne__(self, other):
+        """Test for inequality
+
+        Keyword Arguments:
+        other - The other item being compared against this one
+        """
+        return not (self.username == other.username)
+
     def __repr__(self):
         """Create a string representation of the object"""
         name = ''

-- 
To view, visit https://gerrit.asterisk.org/132
To unsubscribe, visit https://gerrit.asterisk.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I81f66a33f8f2ff7a7d9d0cbb82428e71370536b2
Gerrit-PatchSet: 2
Gerrit-Project: repotools
Gerrit-Branch: master
Gerrit-Owner: Matt Jordan <mjordan at digium.com>



More information about the asterisk-code-review mailing list