Skip to content
This repository was archived by the owner on Mar 14, 2023. It is now read-only.

Commit 4c6ce3e

Browse files
authored
Merge pull request #119 from davidalber/test-new-contrib-with-commit-search
Using commit search to identify new contributors
2 parents 0440d9c + 9636e3c commit 4c6ce3e

File tree

2 files changed

+87
-17
lines changed

2 files changed

+87
-17
lines changed

highfive/newpr.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
user_collabo_url = "https://api.github.com/repos/%s/%s/collaborators/%s"
2424
issue_url = "https://api.github.com/repos/%s/%s/issues/%s"
2525
issue_labels_url = "https://api.github.com/repos/%s/%s/issues/%s/labels"
26+
commit_search_url = "https://api.github.com/search/commits?q=repo:%s/%s+author:%s"
2627

2728
welcome_with_reviewer = '@%s (or someone else)'
2829
welcome_without_reviewer = "@nrc (NB. this repo may be misconfigured)"
@@ -165,24 +166,24 @@ def parse_header_links(value):
165166

166167
return links
167168

168-
def is_new_contributor(username, owner, repo, user, token, config):
169-
if 'contributors' in config and username in config['contributors']:
169+
def is_new_contributor(username, owner, repo, user, token, payload):
170+
# If this is a fork, we do not treat anyone as a new user. This is
171+
# because the API endpoint called in this function indicates all
172+
# users in repository forks have zero commits.
173+
if payload['repository']['fork']:
170174
return False
171175

172-
# iterate through the pages to try and find the contributor
173-
url = contributors_url % (owner, repo)
174-
while True:
175-
stats_raw = api_req("GET", url, None, user, token)
176-
stats = json.loads(stats_raw['body'])
177-
links = parse_header_links(stats_raw['header'].get('Link'))
178-
179-
for contributor in stats:
180-
if contributor['login'] == username:
181-
return False
182-
183-
if not links or 'next' not in links:
184-
return True
185-
url = links['next']
176+
try:
177+
result = api_req(
178+
'GET', commit_search_url % (owner, repo, username), None, user, token,
179+
'application/vnd.github.cloak-preview'
180+
)
181+
return json.loads(result['body'])['total_count'] > 0
182+
except urllib2.HTTPError, e:
183+
if e.code == 422:
184+
return False
185+
else:
186+
raise e
186187

187188
# If the user specified a reviewer, return the username, otherwise returns None.
188189
def find_reviewer(msg):
@@ -374,7 +375,7 @@ def new_pr(payload, user, token):
374375

375376
set_assignee(reviewer, owner, repo, issue, user, token, author, to_mention)
376377

377-
if is_new_contributor(author, owner, repo, user, token, config):
378+
if is_new_contributor(author, owner, repo, user, token, payload):
378379
post_comment(welcome_msg(reviewer, config), owner, repo, issue, user, token)
379380
elif post_msg:
380381
post_comment(review_msg(reviewer, author), owner, repo, issue, user, token)

highfive/tests/test_newpr.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,75 @@ def test_no_assignee(self):
576576
self.mocks['client'].send_then_quit.assert_not_called()
577577
self.mocks['post_comment'].assert_not_called()
578578

579+
class TestIsNewContributor(TestNewPR):
580+
@classmethod
581+
def setUpClass(cls):
582+
cls.username = 'commitUser'
583+
cls.owner = 'repo-owner'
584+
cls.repo = 'repo-name'
585+
cls.user = 'integrationUser'
586+
cls.token = 'credential'
587+
588+
def setUp(self):
589+
super(TestIsNewContributor, self).setUp()
590+
self.payload = {'repository': {'fork': False}}
591+
self.patchers = {
592+
'api_req': mock.patch('highfive.newpr.api_req'),
593+
}
594+
self.mocks = {k: v.start() for k,v in self.patchers.iteritems()}
595+
596+
def tearDown(self):
597+
super(TestIsNewContributor, self).tearDown()
598+
599+
for patcher in self.patchers.itervalues():
600+
patcher.stop()
601+
602+
def is_new_contributor(self):
603+
return newpr.is_new_contributor(
604+
self.username, self.owner, self.repo, self.user, self.token,
605+
self.payload
606+
)
607+
608+
def api_return(self, total_count):
609+
return {
610+
'body': json.dumps({'total_count': total_count}),
611+
'header': {},
612+
}
613+
614+
def assert_api_req_call(self):
615+
self.mocks['api_req'].assert_called_once_with(
616+
'GET',
617+
'https://api.github.com/search/commits?q=repo:%s/%s+author:%s' % (
618+
self.owner, self.repo, self.username
619+
), None, self.user, self.token,
620+
'application/vnd.github.cloak-preview'
621+
)
622+
623+
def test_is_new_contributor_fork(self):
624+
self.payload['repository']['fork'] = True
625+
self.assertFalse(self.is_new_contributor())
626+
self.mocks['api_req'].assert_not_called()
627+
628+
def test_is_new_contributor_has_commits(self):
629+
self.mocks['api_req'].return_value = self.api_return(5)
630+
self.assertTrue(self.is_new_contributor())
631+
self.assert_api_req_call()
632+
633+
def test_is_new_contributor_no_commits(self):
634+
self.mocks['api_req'].return_value = self.api_return(0)
635+
self.assertFalse(self.is_new_contributor())
636+
self.assert_api_req_call()
637+
638+
def test_is_new_contributor_nonexistent_user(self):
639+
self.mocks['api_req'].side_effect = HTTPError(None, 422, None, None, None)
640+
self.assertFalse(self.is_new_contributor())
641+
self.assert_api_req_call()
642+
643+
def test_is_new_contributor_error(self):
644+
self.mocks['api_req'].side_effect = HTTPError(None, 403, None, None, None)
645+
self.assertRaises(HTTPError, self.is_new_contributor)
646+
self.assert_api_req_call()
647+
579648
class TestPostWarnings(TestNewPR):
580649
@classmethod
581650
def setUpClass(cls):

0 commit comments

Comments
 (0)