Skip to content

Commit d40d483

Browse files
authored
Merge pull request github#261 from github/multi-repo-search-queries
fix: handle multiple repos in search query
2 parents f923cf7 + 3e0b7a0 commit d40d483

File tree

2 files changed

+77
-155
lines changed

2 files changed

+77
-155
lines changed

issue_metrics.py

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
Functions:
99
get_env_vars() -> EnvVars: Get the environment variables for use
1010
in the script.
11-
search_issues(search_query: str, github_connection: github3.GitHub)
11+
search_issues(search_query: str, github_connection: github3.GitHub, owners_and_repositories: List[dict])
1212
-> github3.structs.SearchIterator:
1313
Searches for issues in a GitHub repository that match the given search query.
1414
get_per_issue_metrics(issues: Union[List[dict], List[github3.issues.Issue]],
@@ -43,7 +43,9 @@
4343

4444

4545
def search_issues(
46-
search_query: str, github_connection: github3.GitHub, owner: str, repository: str
46+
search_query: str,
47+
github_connection: github3.GitHub,
48+
owners_and_repositories: List[dict],
4749
) -> List[github3.search.IssueSearchResult]: # type: ignore
4850
"""
4951
Searches for issues/prs/discussions in a GitHub repository that match
@@ -52,8 +54,8 @@ def search_issues(
5254
Args:
5355
search_query (str): The search query to use for finding issues/prs/discussions.
5456
github_connection (github3.GitHub): A connection to the GitHub API.
55-
owner (str): The owner of the repository to search in.
56-
repository (str): The repository to search in.
57+
owners_and_repositories (List[dict]): A list of dictionaries containing
58+
the owner and repository names.
5759
5860
Returns:
5961
List[github3.search.IssueSearchResult]: A list of issues that match the search query.
@@ -63,18 +65,22 @@ def search_issues(
6365

6466
# Print the issue titles
6567
issues = []
68+
repos_and_owners_string = ""
69+
for item in owners_and_repositories:
70+
repos_and_owners_string += f"{item['owner']}/{item['repository']} "
71+
6672
try:
6773
for issue in issues_iterator:
6874
print(issue.title) # type: ignore
6975
issues.append(issue)
7076
except github3.exceptions.ForbiddenError:
7177
print(
72-
f"You do not have permission to view this repository '{repository}'; Check your API Token."
78+
f"You do not have permission to view a repository from: '{repos_and_owners_string}'; Check your API Token."
7379
)
7480
sys.exit(1)
7581
except github3.exceptions.NotFoundError:
7682
print(
77-
f"The repository could not be found; Check the repository owner and name: '{owner}/{repository}"
83+
f"The repository could not be found; Check the repository owner and names: '{repos_and_owners_string}"
7884
)
7985
sys.exit(1)
8086
except github3.exceptions.ConnectionError:
@@ -212,28 +218,35 @@ def get_per_issue_metrics(
212218
return issues_with_metrics, num_issues_open, num_issues_closed
213219

214220

215-
def get_owner_and_repository(
221+
def get_owners_and_repositories(
216222
search_query: str,
217-
) -> dict:
218-
"""Get the owner and repository from the search query.
223+
) -> List[dict]:
224+
"""Get the owners and repositories from the search query.
219225
220226
Args:
221227
search_query (str): The search query used to search for issues.
222228
223229
Returns:
224-
dict: A dictionary of owner and repository.
230+
List[dict]: A list of dictionaries of owners and repositories.
225231
226232
"""
227233
search_query_split = search_query.split(" ")
228-
result = {}
234+
results_list = []
229235
for item in search_query_split:
236+
result = {}
230237
if "repo:" in item and "/" in item:
231238
result["owner"] = item.split(":")[1].split("/")[0]
232239
result["repository"] = item.split(":")[1].split("/")[1]
233240
if "org:" in item or "owner:" in item or "user:" in item:
234241
result["owner"] = item.split(":")[1]
242+
if "user:" in item:
243+
result["owner"] = item.split(":")[1]
244+
if "owner:" in item:
245+
result["owner"] = item.split(":")[1]
246+
if result:
247+
results_list.append(result)
235248

236-
return result
249+
return results_list
237250

238251

239252
def main():
@@ -279,17 +292,17 @@ def main():
279292
max_comments_eval = int(env_vars.max_comments_eval)
280293
heavily_involved_cutoff = int(env_vars.heavily_involved_cutoff)
281294

282-
# Get the owner and repository from the search query
283-
owner_and_repository = get_owner_and_repository(search_query)
284-
owner = owner_and_repository.get("owner")
285-
repository = owner_and_repository.get("repository")
295+
# Get the owners and repositories from the search query
296+
owners_and_repositories = get_owners_and_repositories(search_query)
286297

287-
if owner is None:
288-
raise ValueError(
289-
"The search query must include a repository owner and name \
290-
(ie. repo:owner/repo), an organization (ie. org:organization), \
291-
a user (ie. user:login) or an owner (ie. owner:user-or-organization)"
292-
)
298+
# Every search query must include a repository owner for each repository, organization, or user
299+
for item in owners_and_repositories:
300+
if item["owner"] is None:
301+
raise ValueError(
302+
"The search query must include a repository owner and name \
303+
(ie. repo:owner/repo), an organization (ie. org:organization), \
304+
a user (ie. user:login) or an owner (ie. owner:user-or-organization)"
305+
)
293306

294307
# Determine if there are label to measure
295308
labels = env_vars.labels_to_measure
@@ -307,7 +320,7 @@ def main():
307320
write_to_markdown(None, None, None, None, None, None, None, None)
308321
return
309322
else:
310-
issues = search_issues(search_query, github_connection, owner, repository)
323+
issues = search_issues(search_query, github_connection, owners_and_repositories)
311324
if len(issues) <= 0:
312325
print("No issues found")
313326
write_to_markdown(None, None, None, None, None, None, None, None)

test_issue_metrics.py

Lines changed: 41 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@
1717
from datetime import datetime, timedelta
1818
from unittest.mock import MagicMock, patch
1919

20-
import issue_metrics
2120
from issue_metrics import (
2221
IssueWithMetrics,
2322
get_env_vars,
24-
get_owner_and_repository,
23+
get_owners_and_repositories,
2524
get_per_issue_metrics,
2625
measure_time_to_close,
2726
measure_time_to_first_response,
@@ -52,43 +51,62 @@ def test_search_issues(self):
5251
mock_connection.search_issues.return_value = mock_issues
5352

5453
# Call search_issues and check that it returns the correct issues
55-
issues = search_issues(
56-
"is:open", mock_connection, "fakeowner", "fakerepository"
57-
)
54+
repo_with_owner = {"owner": "owner1", "repository": "repo1"}
55+
owners_and_repositories = [repo_with_owner]
56+
issues = search_issues("is:open", mock_connection, owners_and_repositories)
5857
self.assertEqual(issues, mock_issues)
5958

6059

6160
class TestGetOwnerAndRepository(unittest.TestCase):
62-
"""Unit tests for the get_owner_and_repository function.
61+
"""Unit tests for the get_owners_and_repositories function.
6362
64-
This class contains unit tests for the get_owner_and_repository function in the
63+
This class contains unit tests for the get_owners_and_repositories function in the
6564
issue_metrics module. The tests use the unittest module and the unittest.mock
6665
module to mock the GitHub API and test the function in isolation.
6766
6867
Methods:
69-
test_get_owner_with_owner_and_repo_in_query: Test get both owner and repo.
70-
test_get_owner_and_repository_with_repo_in_query: Test get just owner.
71-
test_get_owner_and_repository_without_either_in_query: Test get neither.
72-
68+
test_get_owners_with_owner_and_repo_in_query: Test get both owner and repo.
69+
test_get_owners_and_repositories_with_repo_in_query: Test get just owner.
70+
test_get_owners_and_repositories_without_either_in_query: Test get neither.
71+
test_get_owners_and_repositories_with_multiple_entries: Test get multiple entries.
7372
"""
7473

75-
def test_get_owner_with_owner_and_repo_in_query(self):
74+
def test_get_owners_with_owner_and_repo_in_query(self):
7675
"""Test get both owner and repo."""
77-
result = get_owner_and_repository("repo:owner1/repo1")
78-
self.assertEqual(result.get("owner"), "owner1")
79-
self.assertEqual(result.get("repository"), "repo1")
76+
result = get_owners_and_repositories("repo:owner1/repo1")
77+
self.assertEqual(result[0].get("owner"), "owner1")
78+
self.assertEqual(result[0].get("repository"), "repo1")
8079

81-
def test_get_owner_and_repository_with_repo_in_query(self):
80+
def test_get_owner_and_repositories_with_repo_in_query(self):
8281
"""Test get just owner."""
83-
result = get_owner_and_repository("org:owner1")
84-
self.assertEqual(result.get("owner"), "owner1")
85-
self.assertIsNone(result.get("repository"))
82+
result = get_owners_and_repositories("org:owner1")
83+
self.assertEqual(result[0].get("owner"), "owner1")
84+
self.assertIsNone(result[0].get("repository"))
8685

87-
def test_get_owner_and_repository_without_either_in_query(self):
86+
def test_get_owners_and_repositories_without_either_in_query(self):
8887
"""Test get neither."""
89-
result = get_owner_and_repository("is:blah")
90-
self.assertIsNone(result.get("owner"))
91-
self.assertIsNone(result.get("repository"))
88+
result = get_owners_and_repositories("is:blah")
89+
self.assertEqual(result, [])
90+
91+
def test_get_owners_and_repositories_with_multiple_entries(self):
92+
"""Test get multiple entries."""
93+
result = get_owners_and_repositories("repo:owner1/repo1 org:owner2")
94+
self.assertEqual(result[0].get("owner"), "owner1")
95+
self.assertEqual(result[0].get("repository"), "repo1")
96+
self.assertEqual(result[1].get("owner"), "owner2")
97+
self.assertIsNone(result[1].get("repository"))
98+
99+
def test_get_owners_and_repositories_with_org(self):
100+
"""Test get org as owner."""
101+
result = get_owners_and_repositories("org:owner1")
102+
self.assertEqual(result[0].get("owner"), "owner1")
103+
self.assertIsNone(result[0].get("repository"))
104+
105+
def test_get_owners_and_repositories_with_user(self):
106+
"""Test get user as owner."""
107+
result = get_owners_and_repositories("user:owner1")
108+
self.assertEqual(result[0].get("owner"), "owner1")
109+
self.assertIsNone(result[0].get("repository"))
92110

93111

94112
class TestGetEnvVars(unittest.TestCase):
@@ -120,115 +138,6 @@ def test_get_env_vars_missing_query(self):
120138
get_env_vars(test=True)
121139

122140

123-
class TestMain(unittest.TestCase):
124-
"""Unit tests for the main function.
125-
126-
This class contains unit tests for the main function in the issue_metrics
127-
module. The tests use the unittest module and the unittest.mock module to
128-
mock the GitHub API and test the function in isolation.
129-
130-
Methods:
131-
test_main: Test that main runs without errors.
132-
test_main_no_issues_found: Test that main handles when no issues are found
133-
134-
"""
135-
136-
@patch("issue_metrics.auth_to_github")
137-
@patch("issue_metrics.search_issues")
138-
@patch("issue_metrics.measure_time_to_first_response")
139-
@patch("issue_metrics.get_stats_time_to_first_response")
140-
@patch.dict(
141-
os.environ,
142-
{
143-
"SEARCH_QUERY": "is:open repo:user/repo",
144-
"GH_TOKEN": "test_token",
145-
},
146-
)
147-
def test_main(
148-
self,
149-
mock_get_stats_time_to_first_response,
150-
mock_measure_time_to_first_response,
151-
mock_search_issues,
152-
mock_auth_to_github,
153-
):
154-
"""Test that main runs without errors."""
155-
# Set up the mock GitHub connection object
156-
mock_connection = MagicMock()
157-
mock_auth_to_github.return_value = mock_connection
158-
159-
# Set up the mock search_issues function
160-
mock_issues = MagicMock(
161-
items=[
162-
MagicMock(title="Issue 1"),
163-
MagicMock(title="Issue 2"),
164-
]
165-
)
166-
167-
mock_search_issues.return_value = mock_issues
168-
169-
# Set up the mock measure_time_to_first_response function
170-
mock_issues_with_ttfr = [
171-
(
172-
"Issue 1",
173-
"https://github.com/user/repo/issues/1",
174-
"alice",
175-
timedelta(days=1, hours=2, minutes=30),
176-
),
177-
(
178-
"Issue 2",
179-
"https://github.com/user/repo/issues/2",
180-
"bob",
181-
timedelta(days=3, hours=4, minutes=30),
182-
),
183-
]
184-
mock_measure_time_to_first_response.return_value = mock_issues_with_ttfr
185-
186-
# Set up the mock get_stats_time_to_first_response function
187-
mock_stats_time_to_first_response = 15
188-
mock_get_stats_time_to_first_response.return_value = (
189-
mock_stats_time_to_first_response
190-
)
191-
192-
# Call main and check that it runs without errors
193-
issue_metrics.main()
194-
195-
# Remove the markdown file created by main
196-
os.remove("issue_metrics.md")
197-
198-
@patch("issue_metrics.auth_to_github")
199-
@patch("issue_metrics.search_issues")
200-
@patch("issue_metrics.write_to_markdown")
201-
@patch.dict(
202-
os.environ,
203-
{
204-
"SEARCH_QUERY": "is:open repo:org/repo",
205-
"GH_TOKEN": "test_token",
206-
},
207-
)
208-
def test_main_no_issues_found(
209-
self,
210-
mock_write_to_markdown,
211-
mock_search_issues,
212-
mock_auth_to_github,
213-
):
214-
"""Test that main writes 'No issues found' to the
215-
console and calls write_to_markdown with None."""
216-
217-
# Set up the mock GitHub connection object
218-
mock_connection = MagicMock()
219-
mock_auth_to_github.return_value = mock_connection
220-
221-
# Set up the mock search_issues function to return an empty list of issues
222-
mock_issues = MagicMock(items=[])
223-
mock_search_issues.return_value = mock_issues
224-
225-
# Call main and check that it writes 'No issues found'
226-
issue_metrics.main()
227-
mock_write_to_markdown.assert_called_once_with(
228-
None, None, None, None, None, None, None, None
229-
)
230-
231-
232141
class TestGetPerIssueMetrics(unittest.TestCase):
233142
"""Test suite for the get_per_issue_metrics function."""
234143

0 commit comments

Comments
 (0)