Repository: VirtusLab/git-machete
Pull request #1293 (2024-08-02 17:55:45)

Stop PR/MR chain in `checkout-{prs,mrs}` at `main`/`master` branch


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 9446943..598a6b1 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,6 +2,7 @@
 
 ## New in git-machete 3.26.4
 
+- fixed: avoid detecting a cycle when there's a PR/MR from `main` or `master` in a fork to the original repo (reported by @Joibel)
 - improved: `git machete git{hub,lab} checkout-{prs,mrs}` only adds annotations to affected branches (and not to every branch)
 
 ## New in git-machete 3.26.3
diff --git a/git_machete/client.py b/git_machete/client.py
index f70080c..fd31b35 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -2200,6 +2200,11 @@ class MacheteClient:
         path: List[PullRequest] = [original_pr]
         pr_base: Optional[str] = original_pr.base
         while pr_base:
+            # The chain needs to stop at main/master branches
+            # to avoid false-positive cycles when there's a PR from main in fork to main in the original repo.
+            # See https://github.com/VirtusLab/git-machete/issues/1276
+            if pr_base in ('main', 'master'):
+                return path
             if pr_base in visited_head_branches:
                 raise MacheteException(f"There is a cycle between {spec.display_name} {spec.pr_short_name}s: " +
                                        " -> ".join(visited_head_branches + [pr_base]))


Test patch:
diff --git a/tests/test_github_checkout_prs.py b/tests/test_github_checkout_prs.py
index abb82e0..113063b 100644
--- a/tests/test_github_checkout_prs.py
+++ b/tests/test_github_checkout_prs.py
@@ -630,3 +630,36 @@ class TestGitHubCheckoutPRs(BaseTest):
             'Checking for open GitHub PRs... OK\n'
             'PR #18 checked out at local branch develop\n'
         )
+
+    @staticmethod
+    def prs_for_test_checkout_prs_main_to_main_pr() -> List[Dict[str, Any]]:
+        return [
+            mock_pr_json(head='fix-10341', base='main', number=2),
+            mock_pr_json(head='main', base='main', number=1, repo_id=2)
+        ]
+
+    def test_github_checkout_prs_main_to_main_pr(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.git_operations.GitContext.fetch_remote', lambda _self, _remote: None)
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none)
+        second_remote_path = self.repo_sandbox.create_repo("second-remote", bare=True)
+        github_api_state = MockGitHubAPIState(
+            self.repositories_for_test_github_checkout_prs(second_remote_path),
+            *self.prs_for_test_checkout_prs_main_to_main_pr())
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(github_api_state))
+
+        (
+            self.repo_sandbox
+            .new_branch("main")
+            .commit()
+            .push()
+            .new_branch("fix-10341")
+            .commit()
+            .push()
+        )
+
+        assert_success(
+            ['github', 'checkout-prs', '2'],
+            'Checking for open GitHub PRs... OK\n'
+            'PR #2 checked out at local branch fix-10341\n'
+        )
diff --git a/tests/test_gitlab_checkout_mrs.py b/tests/test_gitlab_checkout_mrs.py
index bb9adc9..2742a16 100644
--- a/tests/test_gitlab_checkout_mrs.py
+++ b/tests/test_gitlab_checkout_mrs.py
@@ -631,3 +631,36 @@ class TestGitLabCheckoutMRs(BaseTest):
             'Checking for open GitLab MRs... OK\n'
             'MR !18 checked out at local branch develop\n'
         )
+
+    @staticmethod
+    def mrs_for_test_checkout_mrs_main_to_main_pr() -> List[Dict[str, Any]]:
+        return [
+            mock_mr_json(head='fix-10341', base='main', number=2),
+            mock_mr_json(head='main', base='main', number=1, repo_id=2)
+        ]
+
+    def test_gitlab_checkout_mrs_main_to_main_mr(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.git_operations.GitContext.fetch_remote', lambda _self, _remote: None)
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.gitlab.GitLabToken.for_domain', mock_gitlab_token_for_domain_none)
+        second_remote_path = self.repo_sandbox.create_repo("second-remote", bare=True)
+        gitlab_api_state = MockGitLabAPIState(
+            self.projects_for_test_gitlab_checkout_prs(second_remote_path),
+            *self.mrs_for_test_checkout_mrs_main_to_main_pr())
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(gitlab_api_state))
+
+        (
+            self.repo_sandbox
+            .new_branch("main")
+            .commit()
+            .push()
+            .new_branch("fix-10341")
+            .commit()
+            .push()
+        )
+
+        assert_success(
+            ['gitlab', 'checkout-mrs', '2'],
+            'Checking for open GitLab MRs... OK\n'
+            'MR !2 checked out at local branch fix-10341\n'
+        )