diff --git a/dashboard/helpers.py b/dashboard/helpers.py
index adcb09c18..d8fc59c88 100644
--- a/dashboard/helpers.py
+++ b/dashboard/helpers.py
@@ -30,21 +30,28 @@ INFINITY_HTML = '∞'
gravatar = gravatar_ext.Gravatar(None, size=64, rating='g', default='wavatar')
-def _extend_record_common_fields(record):
- record['date_str'] = format_datetime(record['date'])
+def _extend_author_fields(record):
record['author_link'] = make_link(
record['author_name'], '/',
{'user_id': record['user_id'], 'company': ''})
record['company_link'] = make_link(
record['company_name'], '/',
{'company': record['company_name'], 'user_id': ''})
+ record['gravatar'] = gravatar(record.get('author_email', 'stackalytics'))
+
+
+def _extend_record_common_fields(record):
+ _extend_author_fields(record)
+ record['date_str'] = format_datetime(record['date'])
record['module_link'] = make_link(
record['module'], '/',
{'module': record['module'], 'company': '', 'user_id': ''})
- record['gravatar'] = gravatar(record.get('author_email', 'stackalytics'))
record['blueprint_id_count'] = len(record.get('blueprint_id', []))
record['bug_id_count'] = len(record.get('bug_id', []))
+ for coauthor in record.get('coauthor') or []:
+ _extend_author_fields(coauthor)
+
def extend_record(record):
record = record.copy()
diff --git a/dashboard/templates/_macros/activity_log.html b/dashboard/templates/_macros/activity_log.html
index c9145b9a1..f541adfc9 100644
--- a/dashboard/templates/_macros/activity_log.html
+++ b/dashboard/templates/_macros/activity_log.html
@@ -72,6 +72,15 @@ show_record_type=True, show_user_gravatar=True, gravatar_size=32, show_all=True)
+ {%if coauthor %}
+
+ {%/if%}
+
{%if record_type == "commit" %}
{%html message %}
diff --git a/stackalytics/processor/record_processor.py b/stackalytics/processor/record_processor.py
index ab46909e5..8e6a75615 100644
--- a/stackalytics/processor/record_processor.py
+++ b/stackalytics/processor/record_processor.py
@@ -14,6 +14,7 @@
# limitations under the License.
import bisect
+import copy
import time
import six
@@ -221,10 +222,25 @@ class RecordProcessor(object):
record['author_email'] = record['author_email'].lower()
record['commit_date'] = record['date']
- self._update_record_and_user(record)
+ coauthors = record.get('coauthor')
+ if not coauthors:
+ self._update_record_and_user(record)
- if record['company_name'] != '*robots':
- yield record
+ if record['company_name'] != '*robots':
+ yield record
+ else:
+ coauthors.append({'author_name': record['author_name'],
+ 'author_email': record['author_email']})
+ for coauthor in coauthors:
+ coauthor['date'] = record['date']
+ self._update_record_and_user(coauthor)
+
+ for coauthor in coauthors:
+ new_record = copy.deepcopy(record)
+ new_record.update(coauthor)
+ new_record['primary_key'] += coauthor['author_email']
+
+ yield new_record
def _spawn_review(self, record):
# copy everything except patchsets and flatten user data
diff --git a/stackalytics/processor/vcs.py b/stackalytics/processor/vcs.py
index 949bab62f..c4ace596a 100644
--- a/stackalytics/processor/vcs.py
+++ b/stackalytics/processor/vcs.py
@@ -73,12 +73,12 @@ MESSAGE_PATTERNS = {
'blueprint_id': re.compile(r'\b(?:blueprint|bp)\b[ \t]*[#:]?[ \t]*'
r'(?P[a-z0-9-]+)', re.IGNORECASE),
'change_id': re.compile('Change-Id: (?PI[0-9a-f]{40})', re.IGNORECASE),
- 'co-author': re.compile(r'(?:Co-Authored|Also)-By:'
- r'\s*(?P.*)\s', re.IGNORECASE)
+ 'coauthor': re.compile(r'(?:Co-Authored|Also)-By:'
+ r'\s*(?P.*)\s', re.IGNORECASE)
}
CO_AUTHOR_PATTERN = re.compile(
- r'(?P.+)\s*<(?P.+)>', re.IGNORECASE)
+ r'(?P.+?)\s*<(?P.+)>', re.IGNORECASE)
class Git(Vcs):
@@ -86,7 +86,7 @@ class Git(Vcs):
def __init__(self, repo, sources_root):
super(Git, self).__init__(repo, sources_root)
uri = self.repo['uri']
- match = re.search(r'([^\/]+)\.git$', uri)
+ match = re.search(r'([^/]+)\.git$', uri)
if match:
self.folder = os.path.normpath(self.sources_root + '/' +
match.group(1))
@@ -212,7 +212,8 @@ class Git(Vcs):
collection = set()
for item in re.finditer(pattern, commit['message']):
collection.add(item.group('id'))
- commit[pattern_name] = list(collection)
+ if collection:
+ commit[pattern_name] = list(collection)
commit['date'] = int(commit['date'])
commit['module'] = self.repo['module']
@@ -227,16 +228,16 @@ class Git(Vcs):
for bp_name
in commit['blueprint_id']]
- yield commit
+ coauthors = []
+ for coauthor in commit.get('coauthor') or []:
+ m = re.match(CO_AUTHOR_PATTERN, coauthor)
+ if utils.check_email_validity(m.group("author_email")):
+ coauthors.append(m.groupdict())
- # Handles co-authors in the commit message. According to the bp
- # we want to count contribution for authors and co-authors.
- if 'co-author' in commit:
- for coauthor in commit['co-author']:
- m = re.match(CO_AUTHOR_PATTERN, coauthor)
- if utils.check_email_validity(m.group("author_email")):
- commit.update(m.groupdict())
- yield commit
+ if coauthors:
+ commit['coauthor'] = coauthors
+
+ yield commit
def get_last_id(self, branch):
LOG.debug('Get head commit for repo uri: %s', self.repo['uri'])
diff --git a/tests/unit/test_record_processor.py b/tests/unit/test_record_processor.py
index ef2767506..5a54567c8 100644
--- a/tests/unit/test_record_processor.py
+++ b/tests/unit/test_record_processor.py
@@ -734,6 +734,44 @@ class TestRecordProcessor(testtools.TestCase):
self.assertEqual(user_2, utils.load_user(runtime_storage_inst,
'homer'))
+ def test_process_commit_with_coauthors(self):
+ record_processor_inst = self.make_record_processor(
+ lp_info={'jimi.hendrix@openstack.com':
+ {'name': 'jimi', 'display_name': 'Jimi Hendrix'},
+ 'tupac.shakur@openstack.com':
+ {'name': 'tupac', 'display_name': 'Tupac Shakur'},
+ 'bob.dylan@openstack.com':
+ {'name': 'bob', 'display_name': 'Bob Dylan'}})
+ processed_commits = list(record_processor_inst.process([
+ {'record_type': 'commit',
+ 'commit_id': 'de7e8f297c193fb310f22815334a54b9c76a0be1',
+ 'author_name': 'Jimi Hendrix',
+ 'author_email': 'jimi.hendrix@openstack.com', 'date': 1234567890,
+ 'lines_added': 25, 'lines_deleted': 9, 'release_name': 'havana',
+ 'coauthor': [{'author_name': 'Tupac Shakur',
+ 'author_email': 'tupac.shakur@openstack.com'},
+ {'author_name': 'Bob Dylan',
+ 'author_email': 'bob.dylan@openstack.com'}]}]))
+
+ self.assertEqual(3, len(processed_commits))
+
+ self.assertRecordsMatch({
+ 'launchpad_id': 'tupac',
+ 'author_email': 'tupac.shakur@openstack.com',
+ 'author_name': 'Tupac Shakur',
+ }, processed_commits[0])
+ self.assertRecordsMatch({
+ 'launchpad_id': 'jimi',
+ 'author_email': 'jimi.hendrix@openstack.com',
+ 'author_name': 'Jimi Hendrix',
+ }, processed_commits[2])
+ self.assertEqual('tupac',
+ processed_commits[0]['coauthor'][0]['user_id'])
+ self.assertEqual('bob',
+ processed_commits[0]['coauthor'][1]['user_id'])
+ self.assertEqual('jimi',
+ processed_commits[0]['coauthor'][2]['user_id'])
+
# record post-processing
def test_blueprint_mention_count(self):
diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py
index 9a0fcc132..73555fdcb 100644
--- a/tests/unit/test_vcs.py
+++ b/tests/unit/test_vcs.py
@@ -117,7 +117,7 @@ diff_stat:
'''
commits = list(self.git.log('dummy', 'dummy'))
- commits_expected = 6 + 2 # authors + co-authors
+ commits_expected = 6
self.assertEqual(commits_expected, len(commits))
self.assertEqual(21, commits[0]['files_changed'])
@@ -144,8 +144,11 @@ diff_stat:
self.assertEqual(0, commits[4]['files_changed'])
self.assertEqual(0, commits[4]['lines_added'])
self.assertEqual(0, commits[4]['lines_deleted'])
+ self.assertFalse('coauthor' in commits[4])
self.assertEqual(
- ['Tupac Shakur ',
- 'Bob Dylan '],
- commits[5]['co-author'])
+ [{'author_name': 'Tupac Shakur',
+ 'author_email': 'tupac.shakur@openstack.com'},
+ {'author_name': 'Bob Dylan',
+ 'author_email': 'bob.dylan@openstack.com'}],
+ commits[5]['coauthor'])