def _assign_bug_to_last_patch_attacher(self, bug_id): committers = CommitterList() bug = self._tool.bugs.fetch_bug(bug_id) if not bug.is_unassigned(): assigned_to_email = bug.assigned_to_email() _log.info(u"Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email))) return reviewed_patches = bug.reviewed_patches() if not reviewed_patches: _log.info("Bug %s has no non-obsolete patches, ignoring." % bug_id) return # We only need to do anything with this bug if one of the r+'d patches does not have a valid committer (cq+ set). if self._patches_have_commiters(reviewed_patches): _log.info("All reviewed patches on bug %s already have commit-queue+, ignoring." % bug_id) return latest_patch = reviewed_patches[-1] attacher_email = latest_patch.attacher_email() committer = committers.committer_by_email(attacher_email) if not committer: _log.info("Attacher %s is not a committer. Bug %s likely needs commit-queue+." % (attacher_email, bug_id)) return reassign_message = u"Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch.id(), committer.full_name) self._tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
def _assign_bug_to_last_patch_attacher(self, bug_id): committers = CommitterList() bug = self._tool.bugs.fetch_bug(bug_id) if not bug.is_unassigned(): assigned_to_email = bug.assigned_to_email() log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email))) return reviewed_patches = bug.reviewed_patches() if not reviewed_patches: log("Bug %s has no non-obsolete patches, ignoring." % bug_id) return # We only need to do anything with this bug if one of the r+'d patches does not have a valid committer (cq+ set). if self._patches_have_commiters(reviewed_patches): log("All reviewed patches on bug %s already have commit-queue+, ignoring." % bug_id) return latest_patch = reviewed_patches[-1] attacher_email = latest_patch.attacher_email() committer = committers.committer_by_email(attacher_email) if not committer: log("Attacher %s is not a committer. Bug %s likely needs commit-queue+." % (attacher_email, bug_id)) return reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch.id(), committer.full_name) self._tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
def _assert_fuzz_match(self, text, name_of_expected_contributor, expected_distance): committers = CommitterList() contributors, distance = committers.contributors_by_fuzzy_match(text) if type(name_of_expected_contributor) is list: expected_names = name_of_expected_contributor else: expected_names = [name_of_expected_contributor] if name_of_expected_contributor else [] self.assertEqual(([contributor.full_name for contributor in contributors], distance), (expected_names, expected_distance))
def __init__(self, options=None): options = options or [] options += [ make_option("--max-commit-age", action="store", dest="max_commit_age", type="int", default=9, help="Specify maximum commit age to consider (in months)."), ] options = sorted(options, cmp=lambda a, b: cmp(a._long_opts, b._long_opts)) super(AbstractCommitLogCommand, self).__init__(options=options) # FIXME: This should probably be on the tool somewhere. self._committer_list = CommitterList()
def __init__(self): options = [ make_option("--committer-minimum", action="store", dest="committer_minimum", type="int", default=10, help="Specify minimum patch count for Committer nominations."), make_option("--reviewer-minimum", action="store", dest="reviewer_minimum", type="int", default=80, help="Specify minimum patch count for Reviewer nominations."), make_option("--max-commit-age", action="store", dest="max_commit_age", type="int", default=9, help="Specify max commit age to consider for nominations (in months)."), make_option("--show-commits", action="store_true", dest="show_commits", default=False, help="Show commit history with nomination suggestions."), ] AbstractDeclarativeCommand.__init__(self, options=options) # FIXME: This should probably be on the tool somewhere. self._committer_list = CommitterList()
def test_commit_info_creation(self): author = Committer("Author", "*****@*****.**") committer = Committer("Committer", "*****@*****.**") reviewer = Reviewer("Reviewer", "*****@*****.**") committer_list = CommitterList(committers=[author, committer], reviewers=[reviewer]) changelog_data = { "bug_id": 1234, "author_name": "Committer", "author_email": "*****@*****.**", "author": author, "reviewer_text": "Reviewer", "reviewer": reviewer, } commit = CommitInfo(123, "*****@*****.**", changelog_data, committer_list) self.assertEqual(commit.revision(), 123) self.assertEqual(commit.bug_id(), 1234) self.assertEqual(commit.author_name(), "Committer") self.assertEqual(commit.author_email(), "*****@*****.**") self.assertEqual(commit.author(), author) self.assertEqual(commit.reviewer_text(), "Reviewer") self.assertEqual(commit.reviewer(), reviewer) self.assertEqual(commit.committer(), committer) self.assertEqual(commit.committer_email(), "*****@*****.**") self.assertEqual(commit.responsible_parties(), set([author, committer, reviewer]))
def execute(self, nick, args, tool, sheriff): if not args: return self.usage(nick) search_string = unicode(" ".join(args)) # FIXME: We should get the ContributorList off the tool somewhere. contributors = CommitterList().contributors_by_search_string( search_string) if not contributors: return unicode( "%s: Sorry, I don't know any contributors matching '%s'.") % ( nick, search_string) if len(contributors) > 5: return unicode( "%s: More than 5 contributors match '%s', could you be more specific?" ) % (nick, search_string) if len(contributors) == 1: contributor = contributors[0] if not contributor.irc_nicknames: return unicode("%s: %s hasn't told me their nick. Boo hoo :-(" ) % (nick, contributor) return unicode("%s: %s is %s. Why do you ask?") % ( nick, search_string, self._full_record_and_nick(contributor)) contributor_nicks = map(self._full_record_and_nick, contributors) contributors_string = join_with_separators(contributor_nicks, only_two_separator=" or ", last_separator=', or ') return unicode("%s: I'm not sure who you mean? %s could be '%s'.") % ( nick, contributors_string, search_string)
def execute(self, nick, args, tool, sheriff): if len(args) != 1: return "%s: Usage: whois SEARCH_STRING" % nick search_string = args[0] # FIXME: We should get the ContributorList off the tool somewhere. contributors = CommitterList().contributors_by_search_string( search_string) if not contributors: return "%s: Sorry, I don't know any contributors matching '%s'." % ( nick, search_string) if len(contributors) > 5: return "%s: More than 5 contributors match '%s', could you be more specific?" % ( nick, search_string) if len(contributors) == 1: contributor = contributors[0] if not contributor.irc_nicknames: return "%s: %s hasn't told me their nick. Boo hoo :-(" % ( nick, contributor) if contributor.emails and search_string.lower() not in map( lambda email: email.lower(), contributor.emails): formattedEmails = ', '.join(contributor.emails) return "%s: %s is %s (%s). Why do you ask?" % ( nick, search_string, self._nick_or_full_record(contributor), formattedEmails) else: return "%s: %s is %s. Why do you ask?" % ( nick, search_string, self._nick_or_full_record(contributor)) contributor_nicks = map(self._nick_or_full_record, contributors) contributors_string = join_with_separators(contributor_nicks, only_two_separator=" or ", last_separator=', or ') return "%s: I'm not sure who you mean? %s could be '%s'." % ( nick, contributors_string, search_string)
def __init__(self, revision, committer_email, changelog_data, committer_list=None): self._revision = revision self._committer_email = committer_email self._changelog_data = changelog_data # Derived values: self._committer = (committer_list or CommitterList()).committer_by_email(committer_email)
def test_commit_queue_flag(self): bugzilla = Bugzilla() bugzilla.committers = CommitterList(reviewers=[Reviewer("WebKit Reviewer", "*****@*****.**")], committers=[Committer("WebKit Committer", "*****@*****.**")], contributors=[Contributor("WebKit Contributor", "*****@*****.**")]) def assert_commit_queue_flag(mark_for_landing, mark_for_commit_queue, expected, username=None): bugzilla.username = username capture = OutputCapture() capture.capture_output() try: self.assertEqual(bugzilla._commit_queue_flag(mark_for_landing=mark_for_landing, mark_for_commit_queue=mark_for_commit_queue), expected) finally: capture.restore_output() assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='******') assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='******') assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='******') assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='?', username='******') assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='******') assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='******') assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='?', username='******') assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='?', username='******') assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='******') assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='******') assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='+', username='******') assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='+', username='******') assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='******') assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='******') assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='+', username='******') assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='+', username='******')
class MockCheckout(object): _committer_list = CommitterList() def commit_info_for_revision(self, svn_revision): return CommitInfo(svn_revision, "*****@*****.**", { "bug_id": 42, "author_name": "Adam Barth", "author_email": "*****@*****.**", "author": self._committer_list.committer_by_email("*****@*****.**"), "reviewer_text": "Darin Adler", "reviewer": self._committer_list.committer_by_name("Darin Adler"), }) def bug_id_for_revision(self, svn_revision): return 12345 def modified_changelogs(self, git_commit, squash): # Ideally we'd return something more interesting here. The problem is # that LandDiff will try to actually read the patch from disk! return [] def commit_message_for_this_commit(self, git_commit, squash): commit_message = Mock() commit_message.message = lambda:"This is a fake commit message that is at least 50 characters." return commit_message def apply_patch(self, patch, force=False): pass def apply_reverse_diff(self, revision): pass
def _summarize_commit_log(self, commit_log, committer_list=CommitterList()): for contributor in committer_list.contributors(): if not contributor.irc_nicknames: continue name_with_nick = "%s (%s)" % (contributor.full_name, contributor.irc_nicknames[0]) if contributor.full_name in commit_log: commit_log = commit_log.replace(contributor.full_name, name_with_nick) for email in contributor.emails: commit_log = commit_log.replace(' <' + email + '>', '') else: for email in contributor.emails: commit_log = commit_log.replace(' %s ' % email, ' %s ' % name_with_nick) patch_by = self._patch_by_regex.search(commit_log) commit_log = self._patch_by_regex.sub('', commit_log, count=1) revert = self._revert_regex.search(commit_log) commit_log = self._revert_regex.sub('', commit_log, count=1) requested_by = self._requested_by_regex.search(commit_log) commit_log = self._bugzilla_url_regex.sub( r'https://webkit.org/b/\g<id>', commit_log) commit_log = self._trac_url_regex.sub( r'https://trac.webkit.org/r\g<revision>', commit_log) lines = commit_log.split('\n')[1:-2] # Ignore lines with ----------. firstline = re.match( r'^(?P<revision>r\d+) \| (?P<email>[^\|]+) \| (?P<timestamp>[^|]+) \| [^\n]+', lines[0]) assert firstline author = firstline.group('email') if patch_by: author = patch_by.group('author') linkified_revision = 'https://trac.webkit.org/%s' % firstline.group( 'revision') lines[0] = '%s by %s' % (linkified_revision, author) if revert: if requested_by: author = requested_by.group('author') contributor = committer_list.contributor_by_irc_nickname( author) if contributor: author = "%s (%s)" % (contributor.full_name, contributor.irc_nicknames[0]) return '%s reverted %s in %s : %s' % ( author, revert.group('revisions'), linkified_revision, requested_by.group('reason')) lines[0] = '%s reverted %s in %s' % ( author, revert.group('revisions'), linkified_revision) return ' '.join(list(filter(lambda line: len(line), lines))[0:4])
def test_commit_queue_flag(self): bugzilla = Bugzilla() bugzilla.committers = CommitterList( reviewers=[Reviewer("WebKit Reviewer", "*****@*****.**")], committers=[Committer("WebKit Committer", "*****@*****.**")], contributors=[ Contributor("WebKit Contributor", "*****@*****.**") ]) def assert_commit_queue_flag(commit_flag, expected, username=None): bugzilla.username = username with OutputCapture(): self.assertEqual(bugzilla._commit_queue_flag(commit_flag), expected) assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_nothing, expected='X', username='******') assert_commit_queue_flag( commit_flag=CommitQueueFlag.mark_for_commit_queue, expected='?', username='******') assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_landing, expected='?', username='******') assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_nothing, expected='X', username='******') assert_commit_queue_flag( commit_flag=CommitQueueFlag.mark_for_commit_queue, expected='?', username='******') assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_landing, expected='?', username='******') assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_nothing, expected='X', username='******') assert_commit_queue_flag( commit_flag=CommitQueueFlag.mark_for_commit_queue, expected='?', username='******') assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_landing, expected='+', username='******') assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_nothing, expected='X', username='******') assert_commit_queue_flag( commit_flag=CommitQueueFlag.mark_for_commit_queue, expected='?', username='******') assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_landing, expected='+', username='******')
class MockCheckout(object): # FIXME: This should move onto the Host object, and we should use a MockCommitterList for tests. _committer_list = CommitterList() def commit_info_for_revision(self, svn_revision): # The real Checkout would probably throw an exception, but this is the only way tests have to get None back at the moment. if not svn_revision: return None return CommitInfo( svn_revision, "*****@*****.**", { "bug_id": 50000, "author_name": "Adam Barth", "author_email": "*****@*****.**", "author": self._committer_list.contributor_by_email("*****@*****.**"), "reviewer_text": "Darin Adler", "reviewer": self._committer_list.committer_by_name("Darin Adler"), "changed_files": [ "path/to/file", "another/file", ], }) def is_path_to_changelog(self, path): # FIXME: This should self._filesystem.basename. return os.path.basename(path) == "ChangeLog" def bug_id_for_revision(self, svn_revision): return 12345 def recent_commit_infos_for_files(self, paths): return [self.commit_info_for_revision(32)] def modified_changelogs(self, git_commit, changed_files=None): # Ideally we'd return something more interesting here. The problem is # that LandDiff will try to actually read the patch from disk! return [] def commit_message_for_this_commit(self, git_commit, changed_files=None): return MockCommitMessage() def chromium_deps(self): return MockDEPS() def apply_patch(self, patch): pass def apply_reverse_diffs(self, revision): pass def suggested_reviewers(self, git_commit, changed_files=None): # FIXME: We should use a shared mock commiter list. return [_mock_reviewers[0]]
def __init__(self, contents, committer_list=CommitterList(), revision=None): self._contents = contents self._committer_list = committer_list self._revision = revision self._parse_entry()
def _validate(self, watch_list): cc_definitions_set = self._rule_definitions_as_set(watch_list.cc_rules) messages_definitions_set = self._rule_definitions_as_set(watch_list.message_rules) self._verify_all_definitions_are_used(watch_list, cc_definitions_set.union(messages_definitions_set)) self._validate_definitions(cc_definitions_set, self._CC_RULES, watch_list) self._validate_definitions(messages_definitions_set, self._MESSAGE_RULES, watch_list) accounts = CommitterList() for cc_rule in watch_list.cc_rules: # Copy the instructions since we'll be remove items from the original list and # modifying a list while iterating through it leads to undefined behavior. intructions_copy = cc_rule.instructions()[:] for email in intructions_copy: if not accounts.contributor_by_email(email): cc_rule.remove_instruction(email) self._log_error("The email alias %s which is in the watchlist is not listed as a contributor in committers.py" % email) continue
def execute(self, nick, args, tool, sheriff): if len(args) != 1: return "%s: Usage: BUGZILLA_EMAIL" % nick email = args[0] committer = CommitterList().committer_by_email(email) if not committer: return "%s: Sorry, I don't know %s. Maybe you could introduce me?" % (nick, email) if not committer.irc_nickname: return "%s: %s hasn't told me their nick. Boo hoo :-(" % (nick, email) return "%s: %s is %s. Why do you ask?" % (nick, email, committer.irc_nickname)
def _needs_commit_queue(patch): if patch.commit_queue() == "+": # If it's already cq+, ignore the patch. log("%s already has cq=%s" % (patch.id(), patch.commit_queue())) return False # We only need to worry about patches from contributers who are not yet committers. committer_record = CommitterList().committer_by_email(patch.attacher_email()) if committer_record: log("%s committer = %s" % (patch.id(), committer_record)) return not committer_record
def check(self, lines): super(ContributorsChecker, self).check(lines) canonicalized = CommitterList().as_json() actual = FileSystem().read_text_file(self._file_path) diff = self._unidiff_output(actual, canonicalized) if diff: self._handle_style_error( 0, 'json/syntax', 5, 'contributors.json differs from the canonical format. Use "validate-committer-lists --canonicalize" to reformat it.' ) print diff
def execute(self, options, args, tool): committers = CommitterList() for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list(): bug = self._tool.bugs.fetch_bug(bug_id) patches = bug.patches(include_obsolete=True) for patch in patches: flags_to_clear = self._flags_to_clear_on_patch(patch) if not flags_to_clear: continue message = u"Cleared %s from obsolete attachment %s so that this bug does not appear in http://webkit.org/pending-commit." % (flags_to_clear, patch.id()) self._tool.bugs.obsolete_attachment(patch.id(), message)
def _validate(self, watch_list): cc_definitions_set = self._rule_definitions_as_set(watch_list.cc_rules) messages_definitions_set = self._rule_definitions_as_set( watch_list.message_rules) self._verify_all_definitions_are_used( watch_list, cc_definitions_set.union(messages_definitions_set)) self._validate_definitions(cc_definitions_set, self._CC_RULES, watch_list) self._validate_definitions(messages_definitions_set, self._MESSAGE_RULES, watch_list) accounts = CommitterList() for cc_rule in watch_list.cc_rules: # Copy the instructions since we'll be remove items from the original list and # modifying a list while iterating through it leads to undefined behavior. intructions_copy = cc_rule.instructions()[:] for email in intructions_copy: if not accounts.contributor_by_email(email): cc_rule.remove_instruction(email) self._log_error( "The email alias %s which is in the watchlist is not listed as a contributor in contributors.json" % email) continue
def __init__(self, revision, committer_email, changelog_data, committer_list=CommitterList()): self._revision = revision self._committer_email = committer_email self._bug_id = changelog_data["bug_id"] self._author_name = changelog_data["author_name"] self._author_email = changelog_data["author_email"] self._author = changelog_data["author"] self._reviewer_text = changelog_data["reviewer_text"] self._reviewer = changelog_data["reviewer"] # Derived values: self._committer = committer_list.committer_by_email(committer_email)
class MockCheckout(object): _committer_list = CommitterList() def commit_info_for_revision(self, svn_revision): # The real Checkout would probably throw an exception, but this is the only way tests have to get None back at the moment. if not svn_revision: return None return CommitInfo(svn_revision, "*****@*****.**", { "bug_id": 42, "author_name": "Adam Barth", "author_email": "*****@*****.**", "author": self._committer_list.committer_by_email("*****@*****.**"), "reviewer_text": "Darin Adler", "reviewer": self._committer_list.committer_by_name("Darin Adler"), }) def bug_id_for_revision(self, svn_revision): return 12345 def recent_commit_infos_for_files(self, paths): return [self.commit_info_for_revision(32)] def modified_changelogs(self, git_commit, changed_files=None): # Ideally we'd return something more interesting here. The problem is # that LandDiff will try to actually read the patch from disk! return [] def commit_message_for_this_commit(self, git_commit, changed_files=None): commit_message = Mock() commit_message.message = lambda:"This is a fake commit message that is at least 50 characters." return commit_message def chromium_deps(self): return MockDEPS() def apply_patch(self, patch, force=False): pass def apply_reverse_diffs(self, revision): pass def suggested_reviewers(self, git_commit, changed_files=None): return [_mock_reviewer]
def _expand_irc_nickname(self, nick): contributor = CommitterList().contributor_by_irc_nickname(nick) if contributor: return str(contributor) return nick
class AbstractCommitLogCommand(Command): _leading_indent_regexp = re.compile(r"^[ ]{4}", re.MULTILINE) _reviewed_by_regexp = re.compile(ChangeLogEntry.reviewed_by_regexp, re.MULTILINE) _patch_by_regexp = re.compile(r'^Patch by (?P<name>.+?)\s+<(?P<email>[^<>]+)> on (?P<date>\d{4}-\d{2}-\d{2})$', re.MULTILINE) _committer_regexp = re.compile(r'^Author: (?P<email>\S+)\s+<[^>]+>$', re.MULTILINE) _date_regexp = re.compile(r'^Date: (?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) [\+\-]\d{4}$', re.MULTILINE) _revision_regexp = re.compile(r'^git-svn-id: http://svn.webkit.org/repository/webkit/trunk@(?P<svnid>\d+) (?P<gitid>[0-9a-f\-]{36})$', re.MULTILINE) def __init__(self, options=None): options = options or [] options += [ make_option("--max-commit-age", action="store", dest="max_commit_age", type="int", default=9, help="Specify maximum commit age to consider (in months)."), ] options = sorted(options, cmp=lambda a, b: cmp(a._long_opts, b._long_opts)) super(AbstractCommitLogCommand, self).__init__(options=options) # FIXME: This should probably be on the tool somewhere. self._committer_list = CommitterList() def _init_options(self, options): self.verbose = options.verbose self.max_commit_age = options.max_commit_age # FIXME: This should move to scm.py def _recent_commit_messages(self): git_log = self._tool.executive.run_command(['git', 'log', '--date=iso', '--since="%s months ago"' % self.max_commit_age]) messages = re.compile(r"^commit \w{40}$", re.MULTILINE).split(git_log)[1:] # Ignore the first message which will be empty. for message in messages: # Unindent all the lines (message, _) = self._leading_indent_regexp.subn("", message) yield message.lstrip() # Remove any leading newlines from the log message. def _author_name_from_email(self, email): contributor = self._committer_list.contributor_by_email(email) return contributor.full_name if contributor else None def _contributor_from_email(self, email): contributor = self._committer_list.contributor_by_email(email) return contributor if contributor else None def _parse_commit_message(self, commit_message): committer_match = self._committer_regexp.search(commit_message) if not committer_match: raise CommitLogError committer_email = committer_match.group('email') if not committer_email: raise CommitLogError committer = self._contributor_from_email(committer_email) if not committer: raise CommitLogError commit_date_match = self._date_regexp.search(commit_message) if not commit_date_match: raise CommitLogError commit_date = commit_date_match.group('date') revision_match = self._revision_regexp.search(commit_message) if not revision_match: raise CommitLogError revision = revision_match.group('svnid') # Look for "Patch by" line first, which is used for non-committer contributors; # otherwise, use committer info determined above. author_match = self._patch_by_regexp.search(commit_message) if not author_match: author_match = committer_match author_email = author_match.group('email') if not author_email: author_email = committer_email author_name = author_match.group('name') if 'name' in author_match.groupdict() else None if not author_name: author_name = self._author_name_from_email(author_email) if not author_name: raise CommitLogError contributor = self._contributor_from_email(author_email) if contributor and author_name != contributor.full_name and contributor.full_name: author_name = contributor.full_name reviewer_match = self._reviewed_by_regexp.search(commit_message) if not reviewer_match: raise CommitLogMissingReviewer reviewers = reviewer_match.group('reviewer') return { 'committer': committer, 'commit_date': commit_date, 'revision': revision, 'author_email': author_email, 'author_name': author_name, 'contributor': contributor, 'reviewers': reviewers, }
class SuggestNominations(AbstractDeclarativeCommand): name = "suggest-nominations" help_text = "Suggest contributors for committer/reviewer nominations" def __init__(self): options = [ make_option("--committer-minimum", action="store", dest="committer_minimum", type="int", default=10, help="Specify minimum patch count for Committer nominations."), make_option("--reviewer-minimum", action="store", dest="reviewer_minimum", type="int", default=80, help="Specify minimum patch count for Reviewer nominations."), make_option("--max-commit-age", action="store", dest="max_commit_age", type="int", default=9, help="Specify max commit age to consider for nominations (in months)."), make_option("--show-commits", action="store_true", dest="show_commits", default=False, help="Show commit history with nomination suggestions."), ] AbstractDeclarativeCommand.__init__(self, options=options) # FIXME: This should probably be on the tool somewhere. self._committer_list = CommitterList() _counters_by_name = {} _counters_by_email = {} def _init_options(self, options): self.committer_minimum = options.committer_minimum self.reviewer_minimum = options.reviewer_minimum self.max_commit_age = options.max_commit_age self.show_commits = options.show_commits self.verbose = options.verbose # FIXME: This should move to scm.py def _recent_commit_messages(self): git_log = self._tool.executive.run_command(['git', 'log', '--since="%s months ago"' % self.max_commit_age]) match_git_svn_id = re.compile(r"\n\n git-svn-id:.*\n", re.MULTILINE) match_get_log_lines = re.compile(r"^\S.*\n", re.MULTILINE) match_leading_indent = re.compile(r"^[ ]{4}", re.MULTILINE) messages = re.split(r"commit \w{40}", git_log)[1:] # Ignore the first message which will be empty. for message in messages: # Remove any lines from git and unindent all the lines (message, _) = match_git_svn_id.subn("", message) (message, _) = match_get_log_lines.subn("", message) (message, _) = match_leading_indent.subn("", message) yield message.lstrip() # Remove any leading newlines from the log message. # e.g. Patch by Eric Seidel <*****@*****.**> on 2011-09-15 patch_by_regexp = r'^Patch by (?P<name>.+?)\s+<(?P<email>[^<>]+)> on (?P<date>\d{4}-\d{2}-\d{2})$' def _count_recent_patches(self): # This entire block could be written as a map/reduce over the messages. for message in self._recent_commit_messages(): # FIXME: This should use ChangeLogEntry to do the entire parse instead # of grabbing at its regexps. dateline_match = re.match(ChangeLogEntry.date_line_regexp, message, re.MULTILINE) if not dateline_match: # Modern commit messages don't just dump the ChangeLog entry, but rather # have a special Patch by line for non-committers. dateline_match = re.search(self.patch_by_regexp, message, re.MULTILINE) if not dateline_match: continue author_email = dateline_match.group("email") if not author_email: continue # We only care about reviewed patches, so make sure it has a valid reviewer line. reviewer_match = re.search(ChangeLogEntry.reviewed_by_regexp, message, re.MULTILINE) # We might also want to validate the reviewer name against the committer list. if not reviewer_match or not reviewer_match.group("reviewer"): continue author_name = dateline_match.group("name") if not author_name: continue if re.search("([^a-zA-Z]and[^a-zA-Z])|(,)|(@)", author_name): # This entry seems to have multiple reviewers, or invalid characters, so reject it. continue svn_id_match = re.search(ChangeLogEntry.svn_id_regexp, message, re.MULTILINE) if svn_id_match: svn_id = svn_id_match.group("svnid") if not svn_id_match or not svn_id: svn_id = "unknown" commit_date = dateline_match.group("date") # See if we already have a contributor with this name or email counter_by_name = self._counters_by_name.get(author_name) counter_by_email = self._counters_by_email.get(author_email) if counter_by_name: if counter_by_email: if counter_by_name != counter_by_email: # Merge these two counters This is for the case where we had # John Smith ([email protected]) and Jonathan Smith ([email protected]) # and just found a John Smith ([email protected]). Now we know the # two names are the same person counter_by_name['names'] |= counter_by_email['names'] counter_by_name['emails'] |= counter_by_email['emails'] counter_by_name['count'] += counter_by_email.get('count', 0) self._counters_by_email[author_email] = counter_by_name else: # Add email to the existing counter self._counters_by_email[author_email] = counter_by_name counter_by_name['emails'] |= set([author_email]) else: if counter_by_email: # Add name to the existing counter self._counters_by_name[author_name] = counter_by_email counter_by_email['names'] |= set([author_name]) else: # Create new counter new_counter = {'names': set([author_name]), 'emails': set([author_email]), 'latest_name': author_name, 'latest_email': author_email, 'commits': ""} self._counters_by_name[author_name] = new_counter self._counters_by_email[author_email] = new_counter assert(self._counters_by_name[author_name] == self._counters_by_email[author_email]) counter = self._counters_by_name[author_name] counter['count'] = counter.get('count', 0) + 1 if svn_id.isdigit(): svn_id = "http://trac.webkit.org/changeset/" + svn_id counter['commits'] += " commit: %s on %s by %s (%s)\n" % (svn_id, commit_date, author_name, author_email) return self._counters_by_email def _collect_nominations(self, counters_by_email): nominations = [] for author_email, counter in counters_by_email.items(): if author_email != counter['latest_email']: continue roles = [] contributor = self._committer_list.contributor_by_email(author_email) author_name = counter['latest_name'] patch_count = counter['count'] if patch_count >= self.committer_minimum and (not contributor or not contributor.can_commit): roles.append("committer") if patch_count >= self.reviewer_minimum and (not contributor or not contributor.can_review): roles.append("reviewer") if roles: nominations.append({ 'roles': roles, 'author_name': author_name, 'author_email': author_email, 'patch_count': patch_count, }) return nominations def _print_nominations(self, nominations): def nomination_cmp(a_nomination, b_nomination): roles_result = cmp(a_nomination['roles'], b_nomination['roles']) if roles_result: return -roles_result count_result = cmp(a_nomination['patch_count'], b_nomination['patch_count']) if count_result: return -count_result return cmp(a_nomination['author_name'], b_nomination['author_name']) for nomination in sorted(nominations, nomination_cmp): # This is a little bit of a hack, but its convienent to just pass the nomination dictionary to the formating operator. nomination['roles_string'] = join_with_separators(nomination['roles']).upper() print "%(roles_string)s: %(author_name)s (%(author_email)s) has %(patch_count)s reviewed patches" % nomination counter = self._counters_by_email[nomination['author_email']] if self.show_commits: print counter['commits'] def _print_counts(self, counters_by_email): def counter_cmp(a_tuple, b_tuple): # split the tuples # the second element is the "counter" structure _, a_counter = a_tuple _, b_counter = b_tuple count_result = cmp(a_counter['count'], b_counter['count']) if count_result: return -count_result return cmp(a_counter['latest_name'].lower(), b_counter['latest_name'].lower()) for author_email, counter in sorted(counters_by_email.items(), counter_cmp): if author_email != counter['latest_email']: continue contributor = self._committer_list.contributor_by_email(author_email) author_name = counter['latest_name'] patch_count = counter['count'] counter['names'] = counter['names'] - set([author_name]) counter['emails'] = counter['emails'] - set([author_email]) alias_list = [] for alias in counter['names']: alias_list.append(alias) for alias in counter['emails']: alias_list.append(alias) if alias_list: print "CONTRIBUTOR: %s (%s) has %d reviewed patches %s" % (author_name, author_email, patch_count, "(aliases: " + ", ".join(alias_list) + ")") else: print "CONTRIBUTOR: %s (%s) has %d reviewed patches" % (author_name, author_email, patch_count) return def execute(self, options, args, tool): self._init_options(options) patch_counts = self._count_recent_patches() nominations = self._collect_nominations(patch_counts) self._print_nominations(nominations) if self.verbose: self._print_counts(patch_counts)
def test_committer_lookup(self): committer = Committer('Test One', '*****@*****.**', 'one') reviewer = Reviewer( 'Test Two', ['*****@*****.**', '*****@*****.**', '*****@*****.**']) contributor = Contributor('Test Three', ['*****@*****.**'], 'three') contributor_with_two_nicknames = Contributor('Other Four', ['*****@*****.**'], ['four', 'otherfour']) committer_list = CommitterList( committers=[committer], reviewers=[reviewer], contributors=[contributor, contributor_with_two_nicknames]) # Test valid committer, reviewer and contributor lookup self.assertEqual(committer_list.committer_by_email('*****@*****.**'), committer) self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.committer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.committer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.contributor_by_email('*****@*****.**'), contributor) # Test valid committer, reviewer and contributor lookup self.assertEqual(committer_list.committer_by_name("Test One"), committer) self.assertEqual(committer_list.committer_by_name("Test Two"), reviewer) self.assertEqual(committer_list.committer_by_name("Test Three"), None) self.assertEqual(committer_list.contributor_by_name("Test Three"), contributor) # Test that the first email is assumed to be the Bugzilla email address (for now) self.assertEqual( committer_list.committer_by_email('*****@*****.**').bugzilla_email(), '*****@*****.**') # Test that a known committer is not returned during reviewer lookup self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), None) self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), None) # and likewise that a known contributor is not returned for committer lookup. self.assertEqual(committer_list.committer_by_email('*****@*****.**'), None) # Test that unknown email address fail both committer and reviewer lookup self.assertEqual(committer_list.committer_by_email('*****@*****.**'), None) self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), None) # Test that emails returns a list. self.assertEqual(committer.emails, ['*****@*****.**']) self.assertEqual(committer.irc_nicknames, ['one']) self.assertEqual(committer_list.contributor_by_irc_nickname('one'), committer) self.assertEqual(committer_list.contributor_by_irc_nickname('three'), contributor) self.assertEqual(committer_list.contributor_by_irc_nickname('four'), contributor_with_two_nicknames) self.assertEqual( committer_list.contributor_by_irc_nickname('otherfour'), contributor_with_two_nicknames) # Test that the lists returned are are we expect them. self.assertEqual( committer_list.contributors(), [contributor, contributor_with_two_nicknames, committer, reviewer]) self.assertEqual(committer_list.committers(), [committer, reviewer]) self.assertEqual(committer_list.reviewers(), [reviewer]) self.assertEqual(committer_list.contributors_by_search_string('test'), [contributor, committer, reviewer]) self.assertEqual(committer_list.contributors_by_search_string('rad'), [reviewer]) self.assertEqual(committer_list.contributors_by_search_string('Two'), [reviewer])
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from .commitinfo import CommitInfo # FIXME: These imports are wrong, we should use a shared MockCommittersList. from webkitpy.common.config.committers import CommitterList from webkitpy.common.net.bugzilla.bugzilla_mock import _mock_reviewers from webkitpy.common.system.filesystem_mock import MockFileSystem class MockCommitMessage(object): def message(self): return "This is a fake commit message that is at least 50 characters." committer_list = CommitterList() mock_revisions = { 1: CommitInfo( 852, "*****@*****.**", { "bug_id": 50000, "author_name": "Adam Barth", "author_email": "*****@*****.**", "author": committer_list.contributor_by_email("*****@*****.**"), "reviewer_text": "Darin Adler", "reviewer": committer_list.committer_by_name("Darin Adler"), "changed_files": [ "path/to/file", "another/file", ],
def execute(self, options, args, tool): search_string = args[0] users = CommitterList().contributors_by_search_string(search_string) for user in users: print(user)
def __init__(self): self.queries = MockBugzillaQueries(self) # FIXME: This should move onto the Host object, and we should use a MockCommitterList self.committers = CommitterList(reviewers=_mock_reviewers) self.username = None self._override_patch = None
def __init__(self, contents, committer_list=CommitterList()): self._contents = contents self._committer_list = committer_list self._parse_entry()
def test_committer_lookup(self): committer = Committer('Test One', '*****@*****.**', 'one') reviewer = Reviewer('Test Two', ['*****@*****.**', '*****@*****.**', '*****@*****.**']) contributor = Contributor('Test Three', ['*****@*****.**'], 'three') contributor_with_two_nicknames = Contributor('Other Four', ['*****@*****.**', '*****@*****.**'], ['four', 'otherfour']) contributor_with_same_email_username = Contributor('Yet Another Four', ['*****@*****.**'], ['yetanotherfour']) committer_list = CommitterList(committers=[committer], reviewers=[reviewer], contributors=[contributor, contributor_with_two_nicknames, contributor_with_same_email_username]) # Test valid committer, reviewer and contributor lookup self.assertEqual(committer_list.committer_by_email('*****@*****.**'), committer) self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.committer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.committer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.contributor_by_email('*****@*****.**'), contributor) # Test valid committer, reviewer and contributor lookup self.assertEqual(committer_list.committer_by_name("Test One"), committer) self.assertEqual(committer_list.committer_by_name("Test Two"), reviewer) self.assertIsNone(committer_list.committer_by_name("Test Three")) self.assertEqual(committer_list.contributor_by_name("Test Three"), contributor) self.assertEqual(committer_list.contributor_by_name("test one"), committer) self.assertEqual(committer_list.contributor_by_name("test two"), reviewer) self.assertEqual(committer_list.contributor_by_name("test three"), contributor) # Test that the first email is assumed to be the Bugzilla email address (for now) self.assertEqual(committer_list.committer_by_email('*****@*****.**').bugzilla_email(), '*****@*****.**') # Test that a known committer is not returned during reviewer lookup self.assertIsNone(committer_list.reviewer_by_email('*****@*****.**')) self.assertIsNone(committer_list.reviewer_by_email('*****@*****.**')) # and likewise that a known contributor is not returned for committer lookup. self.assertIsNone(committer_list.committer_by_email('*****@*****.**')) # Test that unknown email address fail both committer and reviewer lookup self.assertIsNone(committer_list.committer_by_email('*****@*****.**')) self.assertIsNone(committer_list.reviewer_by_email('*****@*****.**')) # Test that emails returns a list. self.assertEqual(committer.emails, ['*****@*****.**']) self.assertEqual(committer.irc_nicknames, ['one']) self.assertEqual(committer_list.contributor_by_irc_nickname('one'), committer) self.assertEqual(committer_list.contributor_by_irc_nickname('three'), contributor) self.assertEqual(committer_list.contributor_by_irc_nickname('four'), contributor_with_two_nicknames) self.assertEqual(committer_list.contributor_by_irc_nickname('otherfour'), contributor_with_two_nicknames) # Test that the lists returned are are we expect them. self.assertEqual(committer_list.contributors(), [contributor, contributor_with_two_nicknames, contributor_with_same_email_username, committer, reviewer]) self.assertEqual(committer_list.committers(), [committer, reviewer]) self.assertEqual(committer_list.reviewers(), [reviewer]) self.assertEqual(committer_list.contributors_by_search_string('test'), [contributor, committer, reviewer]) self.assertEqual(committer_list.contributors_by_search_string('rad'), [reviewer]) self.assertEqual(committer_list.contributors_by_search_string('Two'), [reviewer]) self.assertEqual(committer_list.contributors_by_search_string('otherfour'), [contributor_with_two_nicknames]) self.assertEqual(committer_list.contributors_by_search_string('*otherfour*'), [contributor_with_two_nicknames, contributor_with_same_email_username]) self.assertEqual(committer_list.contributors_by_email_username("one"), [committer]) self.assertEqual(committer_list.contributors_by_email_username("four"), []) self.assertEqual(committer_list.contributors_by_email_username("otherfour"), [contributor_with_two_nicknames, contributor_with_same_email_username])
def test_committer_lookup(self): committer = Committer('Test One', '*****@*****.**', 'one') reviewer = Reviewer('Test Two', ['*****@*****.**', '*****@*****.**', '*****@*****.**']) committer_list = CommitterList(committers=[committer], reviewers=[reviewer]) # Test valid committer and reviewer lookup self.assertEqual(committer_list.committer_by_email('*****@*****.**'), committer) self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.committer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.committer_by_email('*****@*****.**'), reviewer) self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), reviewer) # Test valid committer and reviewer lookup self.assertEqual(committer_list.committer_by_name("Test One"), committer) self.assertEqual(committer_list.committer_by_name("Test Two"), reviewer) self.assertEqual(committer_list.committer_by_name("Test Three"), None) # Test that the first email is assumed to be the Bugzilla email address (for now) self.assertEqual(committer_list.committer_by_email('*****@*****.**').bugzilla_email(), '*****@*****.**') # Test that a known committer is not returned during reviewer lookup self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), None) # Test that unknown email address fail both committer and reviewer lookup self.assertEqual(committer_list.committer_by_email('*****@*****.**'), None) self.assertEqual(committer_list.reviewer_by_email('*****@*****.**'), None) # Test that emails returns a list. self.assertEqual(committer.emails, ['*****@*****.**']) self.assertEqual(committer.irc_nickname, 'one') # Test that committers returns committers and reviewers and reviewers() just reviewers. self.assertEqual(committer_list.committers(), [committer, reviewer]) self.assertEqual(committer_list.reviewers(), [reviewer])
def __init__(self): Mock.__init__(self) self.queries = MockBugzillaQueries(self) self.committers = CommitterList(reviewers=[Reviewer("Foo Bar", "*****@*****.**")])
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from .commitinfo import CommitInfo # FIXME: These imports are wrong, we should use a shared MockCommittersList. from webkitpy.common.config.committers import CommitterList from webkitpy.common.net.bugzilla.bugzilla_mock import _mock_reviewers from webkitpy.common.system.filesystem_mock import MockFileSystem class MockCommitMessage(object): def message(self): return "This is a fake commit message that is at least 50 characters." committer_list = CommitterList() mock_revisions = { 1: CommitInfo(852, "*****@*****.**", { "bug_id": 50000, "author_name": "Adam Barth", "author_email": "*****@*****.**", "author": committer_list.contributor_by_email("*****@*****.**"), "reviewer_text": "Darin Adler", "reviewer": committer_list.committer_by_name("Darin Adler"), "changed_files": [ "path/to/file", "another/file", ], "bug_description": "Example description of bug 50000.", }),
def test_committer_lookup(self): account = Account("Test Zero", ["*****@*****.**", "*****@*****.**"], "zero") committer = Committer("Test One", "*****@*****.**", "one") reviewer = Reviewer("Test Two", ["*****@*****.**", "*****@*****.**", "*****@*****.**"]) contributor = Contributor("Test Three", ["*****@*****.**"], "three") contributor_with_two_nicknames = Contributor( "Other Four", ["*****@*****.**", "*****@*****.**"], ["four", "otherfour"] ) contributor_with_same_email_username = Contributor( "Yet Another Four", ["*****@*****.**"], ["yetanotherfour"] ) committer_list = CommitterList( watchers=[account], committers=[committer], reviewers=[reviewer], contributors=[contributor, contributor_with_two_nicknames, contributor_with_same_email_username], ) # Test valid committer, reviewer and contributor lookup self.assertEqual(committer_list.account_by_email("*****@*****.**"), account) self.assertEqual(committer_list.committer_by_email("*****@*****.**"), committer) self.assertEqual(committer_list.reviewer_by_email("*****@*****.**"), reviewer) self.assertEqual(committer_list.committer_by_email("*****@*****.**"), reviewer) self.assertEqual(committer_list.committer_by_email("*****@*****.**"), reviewer) self.assertEqual(committer_list.reviewer_by_email("*****@*****.**"), reviewer) self.assertEqual(committer_list.contributor_by_email("*****@*****.**"), contributor) # Test valid committer, reviewer and contributor lookup self.assertEqual(committer_list.committer_by_name("Test One"), committer) self.assertEqual(committer_list.committer_by_name("Test Two"), reviewer) self.assertEqual(committer_list.committer_by_name("Test Three"), None) self.assertEqual(committer_list.contributor_by_name("Test Three"), contributor) self.assertEqual(committer_list.contributor_by_name("test one"), committer) self.assertEqual(committer_list.contributor_by_name("test two"), reviewer) self.assertEqual(committer_list.contributor_by_name("test three"), contributor) # Test that the first email is assumed to be the Bugzilla email address (for now) self.assertEqual(committer_list.committer_by_email("*****@*****.**").bugzilla_email(), "*****@*****.**") # Test lookup by login email address self.assertEqual(committer_list.account_by_login("*****@*****.**"), account) self.assertEqual(committer_list.account_by_login("*****@*****.**"), None) self.assertEqual(committer_list.account_by_login("*****@*****.**"), committer) self.assertEqual(committer_list.account_by_login("*****@*****.**"), reviewer) self.assertEqual(committer_list.account_by_login("*****@*****.**"), None) self.assertEqual(committer_list.account_by_login("*****@*****.**"), None) # Test that a known committer is not returned during reviewer lookup self.assertEqual(committer_list.reviewer_by_email("*****@*****.**"), None) self.assertEqual(committer_list.reviewer_by_email("*****@*****.**"), None) # and likewise that a known contributor is not returned for committer lookup. self.assertEqual(committer_list.committer_by_email("*****@*****.**"), None) # Test that unknown email address fail both committer and reviewer lookup self.assertEqual(committer_list.committer_by_email("*****@*****.**"), None) self.assertEqual(committer_list.reviewer_by_email("*****@*****.**"), None) # Test that emails returns a list. self.assertEqual(committer.emails, ["*****@*****.**"]) self.assertEqual(committer.irc_nicknames, ["one"]) self.assertEqual(committer_list.contributor_by_irc_nickname("one"), committer) self.assertEqual(committer_list.contributor_by_irc_nickname("three"), contributor) self.assertEqual(committer_list.contributor_by_irc_nickname("four"), contributor_with_two_nicknames) self.assertEqual(committer_list.contributor_by_irc_nickname("otherfour"), contributor_with_two_nicknames) # Test that the lists returned are are we expect them. self.assertEqual( committer_list.contributors(), [contributor, contributor_with_two_nicknames, contributor_with_same_email_username, committer, reviewer], ) self.assertEqual(committer_list.committers(), [committer, reviewer]) self.assertEqual(committer_list.reviewers(), [reviewer]) self.assertEqual(committer_list.contributors_by_search_string("test"), [contributor, committer, reviewer]) self.assertEqual(committer_list.contributors_by_search_string("rad"), [reviewer]) self.assertEqual(committer_list.contributors_by_search_string("Two"), [reviewer]) self.assertEqual(committer_list.contributors_by_search_string("otherfour"), [contributor_with_two_nicknames]) self.assertEqual( committer_list.contributors_by_search_string("*otherfour*"), [contributor_with_two_nicknames, contributor_with_same_email_username], ) self.assertEqual(committer_list.contributors_by_email_username("one"), [committer]) self.assertEqual(committer_list.contributors_by_email_username("four"), []) self.assertEqual( committer_list.contributors_by_email_username("otherfour"), [contributor_with_two_nicknames, contributor_with_same_email_username], )