(repo|submodule|txt)_roller: Attribute rolls

For single-commit rolls attribute rolls to the original commit author.
Hide this behind a property since the permissions may not be set up on
all necessary gerrit hosts.

Added code to keep track of author, owner, and reviewer names as well as
email addresses, so the name can be used in the attribution as well.

Changed testing code to use the same account for both author and owner
in most cases, since the attribution code wouldn't be used if they
differ. It's possible something relating to one of those and not the
other could regress since they're no longer independently tested, but
unlikely.

Bug: 595
Change-Id: I720b33d9b994bba7b9427b8e64cd21dbf4bb7e4b
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/77760
Reviewed-by: Anthony Fandrianto <atyfto@google.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
diff --git a/recipe_modules/roll_util/api.py b/recipe_modules/roll_util/api.py
index 964033a..d522cb4 100644
--- a/recipe_modules/roll_util/api.py
+++ b/recipe_modules/roll_util/api.py
@@ -84,6 +84,10 @@
     REBASE = 'REBASE'
 
 
+# Using a namedtuple instead of attrs because this should be hashable.
+Account = collections.namedtuple('Account', 'name email')
+
+
 @attr.s
 class Commit(object):
     hash = attr.ib(type=str)
@@ -125,7 +129,7 @@
 
         log_cmd = [
             'log',
-            '--pretty=format:%H\n%ae\n%B',
+            '--pretty=format:%H\n%an\n%ae\n%B',
             # Separate entries with null bytes since most entries
             # will contain newlines ("%B" is the full commit
             # message, not just the first line.)
@@ -149,7 +153,8 @@
 
         commits = []
         for i, commit in enumerate(commit_log):
-            commit_hash, author, message = commit.split('\n', 2)
+            commit_hash, name, email, message = commit.split('\n', 3)
+            author = Account(name, email)
             owner = None
             reviewers = []
 
@@ -178,12 +183,24 @@
                     host=full_host,
                     test_data=self._api.json.test_api.output(
                         {
-                            'owner': {'email': 'owner@example.com'},
+                            'owner': {
+                                'name': 'author',
+                                'email': 'author@example.com',
+                            },
                             'reviewers': {
                                 'REVIEWER': [
-                                    {'email': 'reviewer@example.com'},
-                                    {'email': 'nobody@google.com'},
-                                    {'email': 'robot@gserviceaccount.com'},
+                                    {
+                                        'name': 'reviewer',
+                                        'email': 'reviewer@example.com',
+                                    },
+                                    {
+                                        'name': 'nobody',
+                                        'email': 'nobody@google.com',
+                                    },
+                                    {
+                                        'name': 'robot',
+                                        'email': 'robot@gserviceaccount.com',
+                                    },
                                 ],
                             },
                         }
@@ -193,9 +210,13 @@
 
                 if step.exc_result.retcode == 0:
                     details = step.json.output
-                    owner = details['owner']['email']
+                    owner = Account(
+                        details['owner']['name'], details['owner']['email']
+                    )
                     for reviewer in details['reviewers']['REVIEWER']:
-                        reviewers.append(reviewer['email'])
+                        reviewers.append(
+                            Account(reviewer['name'], reviewer['email']),
+                        )
 
             commits.append(
                 Commit(
@@ -285,6 +306,14 @@
                     authors.add(commit.owner)
         return authors
 
+    def fake_author(self, author):
+        # Update the author's email address so it can be used for attribution
+        # without literally attributing it to the author's account in Gerrit.
+        return Account(
+            author.name,
+            '{}@pigweed.infra.roller.{}'.format(*author.email.split('@')),
+        )
+
     def reviewers(self, *roll):
         reviewers = set()
         for r in roll:
@@ -303,16 +332,16 @@
             email, 'email:{}'.format(email), host=host, test_data=test_data,
         ).json.output
 
-    def include_cc(self, email, cc_domains, host):
-        with self.m.step.nest('cc {}'.format(email)) as pres:
-            domain = email.split('@', 1)[1]
+    def include_cc(self, account, cc_domains, host):
+        with self.m.step.nest('cc {}'.format(account.email)) as pres:
+            domain = account.email.split('@', 1)[1]
             if domain.endswith('gserviceaccount.com'):
                 pres.step_summary_text = 'not CCing, robot account'
                 return False
             if cc_domains and domain not in cc_domains:
                 pres.step_summary_text = 'not CCing, domain excluded'
                 return False
-            if not self.can_cc_on_roll(email, host=host):
+            if not self.can_cc_on_roll(account.email, host=host):
                 pres.step_summary_text = 'not CCing, no account in Gerrit'
                 return False
 
@@ -422,6 +451,9 @@
 
         return '\n\n'.join(texts)
 
+    def Account(self, name, email):
+        return Account(name, email)
+
     def Roll(self, **kwargs):
         """Create a Roll. See Roll class above for details."""
         return Roll(api=self.m, **kwargs)