diff --git a/README.md b/README.md index 8f2f84b..251f6c9 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ prfiesta -u charliermarsh --after 2023-05-01 --use-reviewed-by # Get all pull requests where the user was requested a review rather then being the author prfiesta -u charliermarsh --after 2023-05-01 --use-review-requested +# Get all pull requests which contains a reference (e.g JIRA card reference) within the PR title or body +prfiesta --reference PA-12765 + # Get help prfiesta --help @@ -128,6 +131,15 @@ When using the `--after` and `--before` date filters, by default `prfiesta` will Learn more about date filters [here](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-by-when-an-issue-or-pull-request-was-created-or-last-updated). +### Reference Search + +You may come across a use case where you want to filter pull requests on a specific reference. For example, it may be a team practise to put a JIRA card reference within the pull request title or body. + +For this you can use the `--reference` filter. + +> [!NOTE] +> Results from reference search is entirely up to the GitHub Search API. On some ocassions, it may not provide inaccurate results. + ## Analysis `prfiesta` ships with built in plots to help analyze your pull request data. These serve as a starting point in your analysis. See more information on the build in plots and views [here](https://github.com/kiran94/prfiesta/blob/main/docs/analysis.md). diff --git a/prfiesta/__main__.py b/prfiesta/__main__.py index 295e161..5761998 100644 --- a/prfiesta/__main__.py +++ b/prfiesta/__main__.py @@ -20,7 +20,8 @@ @cloup.command() @cloup.option_group( "general options", - cloup.option("-u", "--users", required=True, multiple=True, help="The GitHub Users to search for. Can be multiple"), + cloup.option("-u", "--users", multiple=True, help="The GitHub Users to search for. Can be multiple"), + cloup.option("--reference", help="A external ticket reference to search for e.g. JIRA-1234"), ) @cloup.option_group( "date filter options", @@ -69,6 +70,7 @@ def main(**kwargs) -> None: use_involves: bool = kwargs.get("use_involves") use_reviewed_by: bool = kwargs.get("use_reviewed_by") use_review_requested: bool = kwargs.get("use_review_requested") + reference: str = kwargs.get("reference") logger.info("[bold green]PR Fiesta 🦜🥳") @@ -84,6 +86,7 @@ def main(**kwargs) -> None: use_involves=use_involves, use_reviewed_by=use_reviewed_by, use_review_requested=use_review_requested, + reference=reference, ) if not pr_frame.empty: diff --git a/prfiesta/collectors/github.py b/prfiesta/collectors/github.py index 39a30ff..298727c 100644 --- a/prfiesta/collectors/github.py +++ b/prfiesta/collectors/github.py @@ -42,15 +42,16 @@ def __init__(self, **kwargs) -> None: def collect( self, - *users: Tuple[str], + *users: Optional[Tuple[str]], after: Optional[datetime] = None, before: Optional[datetime] = None, use_updated: Optional[bool] = False, use_involves: Optional[bool] = False, use_reviewed_by: Optional[bool] = False, use_review_requested: Optional[bool] = False, + reference: Optional[str] = None, ) -> pd.DataFrame: - query = self._construct_query(users, after, before, use_updated, use_involves, use_reviewed_by, use_review_requested) + query = self._construct_query(users, after, before, use_updated, use_involves, use_reviewed_by, use_review_requested, reference) update_spinner(f"Searching {self._url} with[bold blue] {query}", self._spinner, logger) @@ -84,13 +85,14 @@ def collect( @staticmethod def _construct_query( - users: List[str], + users: Optional[List[str]], after: Optional[datetime] = None, before: Optional[datetime] = None, use_updated: Optional[bool] = False, use_involves: Optional[bool] = False, use_reviewed_by: Optional[bool] = False, use_review_requested: Optional[bool] = False, + reference: Optional[str] = None, ) -> str: """ Constructs a GitHub Search Query @@ -105,6 +107,7 @@ def _construct_query( type:pr involves:user2 type:pr reviewed-by:user1 type:pr review-requested:user1 + type:pr in:title,body "PA-12765" All dates are inclusive. See GitHub Docs for full options https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests @@ -138,6 +141,9 @@ def _construct_query( elif after: query.append(f"{time_filter}:>={after.strftime('%Y-%m-%d')}") + if reference: + query.append(f'in:title,body "{reference}"') + return " ".join(query) def _move_column_to_end(self, df: pd.DataFrame) -> pd.DataFrame: diff --git a/tests/collectors/test_github.py b/tests/collectors/test_github.py index e00049d..77036f6 100644 --- a/tests/collectors/test_github.py +++ b/tests/collectors/test_github.py @@ -148,6 +148,13 @@ "type:pr review-requested:user1 review-requested:user2", id="review_requested_user", ), + pytest.param( + (), + {"reference": "JIRA-1234"}, + [_mock_issue1, _mock_issue2], + 'type:pr in:title,body "JIRA-1234"', + id="reference", + ), ], ) @patch("prfiesta.collectors.github.Github") diff --git a/tests/test_main.py b/tests/test_main.py index 1a09722..6ec3a96 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -12,14 +12,6 @@ FAILURE_CODE = 2 -def test_main_missing_users() -> None: - runner = CliRunner() - result = runner.invoke(main, [""]) - - assert "Missing option '-u' / '--users'" in result.output - assert result.exit_code == FAILURE_CODE - - @pytest.mark.parametrize( "arguments", [ @@ -42,7 +34,18 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: [ pytest.param( ["--users", "test_user"], - [call("test_user", after=None, before=None, use_updated=False, use_involves=False, use_reviewed_by=False, use_review_requested=False)], + [ + call( + "test_user", + after=None, + before=None, + use_updated=False, + use_involves=False, + use_reviewed_by=False, + use_review_requested=False, + reference=None, + ) + ], pd.DataFrame(), "csv", None, @@ -59,6 +62,7 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: use_involves=False, use_reviewed_by=False, use_review_requested=False, + reference=None, ), ], pd.DataFrame(), @@ -77,6 +81,7 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: use_involves=False, use_reviewed_by=False, use_review_requested=False, + reference=None, ), ], pd.DataFrame(), @@ -95,6 +100,7 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: use_involves=False, use_reviewed_by=False, use_review_requested=False, + reference=None, ), ], pd.DataFrame(), @@ -104,7 +110,18 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: ), pytest.param( ["--users", "test_user"], - [call("test_user", after=None, before=None, use_updated=False, use_involves=False, use_reviewed_by=False, use_review_requested=False)], + [ + call( + "test_user", + after=None, + before=None, + use_updated=False, + use_involves=False, + use_reviewed_by=False, + use_review_requested=False, + reference=None, + ) + ], pd.DataFrame( data=[(1, 2, 3)], columns=["col1", "col2", "col3"], @@ -115,7 +132,18 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: ), pytest.param( ["--users", "test_user", "--output-type", "parquet"], - [call("test_user", after=None, before=None, use_updated=False, use_involves=False, use_reviewed_by=False, use_review_requested=False)], + [ + call( + "test_user", + after=None, + before=None, + use_updated=False, + use_involves=False, + use_reviewed_by=False, + use_review_requested=False, + reference=None, + ) + ], pd.DataFrame( data=[(1, 2, 3)], columns=["col1", "col2", "col3"], @@ -135,6 +163,7 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: use_involves=False, use_reviewed_by=False, use_review_requested=False, + reference=None, ), ], pd.DataFrame(), @@ -153,6 +182,7 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: use_involves=True, use_reviewed_by=False, use_review_requested=False, + reference=None, ), ], pd.DataFrame(), @@ -162,7 +192,18 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: ), pytest.param( ["--users", "test_user", "--use-reviewed-by"], - [call("test_user", after=None, before=None, use_updated=False, use_involves=False, use_reviewed_by=True, use_review_requested=False)], + [ + call( + "test_user", + after=None, + before=None, + use_updated=False, + use_involves=False, + use_reviewed_by=True, + use_review_requested=False, + reference=None, + ) + ], pd.DataFrame(), "csv", None, @@ -170,7 +211,18 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: ), pytest.param( ["--users", "test_user", "--use-review-requested"], - [call("test_user", after=None, before=None, use_updated=False, use_involves=False, use_reviewed_by=False, use_review_requested=True)], + [ + call( + "test_user", + after=None, + before=None, + use_updated=False, + use_involves=False, + use_reviewed_by=False, + use_review_requested=True, + reference=None, + ) + ], pd.DataFrame(), "csv", None, @@ -178,12 +230,41 @@ def test_main_author_filters_mutually_exclusive(arguments: List[str]) -> None: ), pytest.param( ["--users", "test_user", "--output-type", "duckdb", "--output", "my.duckdb"], - [call("test_user", after=None, before=None, use_updated=False, use_involves=False, use_reviewed_by=False, use_review_requested=False)], + [ + call( + "test_user", + after=None, + before=None, + use_updated=False, + use_involves=False, + use_reviewed_by=False, + use_review_requested=False, + reference=None, + ) + ], pd.DataFrame(data={"a": [1, 2, 3]}), "duckdb", "my.duckdb", id="with_duckdb", ), + pytest.param( + ["--reference", "JIRA-1234"], + [ + call( + after=None, + before=None, + use_updated=False, + use_involves=False, + use_reviewed_by=False, + use_review_requested=False, + reference="JIRA-1234", + ) + ], + pd.DataFrame(data={"a": [1, 2, 3]}), + "csv", + None, + id="with_reference", + ), ], ) @patch("prfiesta.__main__.Spinner")