Repository: VirtusLab/git-machete
Pull request #1237 (2024-04-25 17:48:06)

In `github create-pr`/`gitlab create-mr`, check whether base/target branch for PR/MR exists in remote, instead of fetching the entire remote
<!-- start git-machete generated -->

# Based on PR #1236

## Full chain of PRs as of 2024-04-25

* PR #1237: `fix/1047-create-pr-fetch-single-refspec` ➔ `fix/qualifiers-outside-traverse`
* PR #1236: `fix/qualifiers-outside-traverse` ➔ `develop`

<!-- end git-machete generated -->


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1237?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.48%. Comparing base [(`5a66b69`)](https://app.codecov.io/gh/VirtusLab/git-machete/commit/5a66b69549ab8e9a49259eba36f31bd644db92b4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) to head [(`5a66b69`)](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1237?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab).

> :exclamation: Current head 5a66b69 differs from pull request most recent head f75681d. Consider uploading reports for the commit f75681d to get more accurate results


<details><summary>Additional details and impacted files</summary>


```diff
@@                       Coverage Diff                        @@
##           fix/qualifiers-outside-traverse    #1237   +/-   ##
================================================================
  Coverage                            98.48%   98.48%           
================================================================
  Files                                   14       14           
  Lines                                 3698     3698           
  Branches                               811      811           
================================================================
  Hits                                  3642     3642           
  Misses                                  37       37           
  Partials                                19       19           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1237?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab).


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 2d4d038..17db617 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -5,6 +5,7 @@
 - fixed: `-y` option in `git machete traverse` automatically sets `--no-edit-merge` flag, to retain behavior when the `update=merge` qualifier is set (contributed by @gjulianm)
 - fixed: `push=no` and `slide-out=no` qualifiers now work in `git machete advance` now
 - fixed: `rebase=no` qualifier now works in `git machete slide-out`
+- improved: in `git machete github create-pr`/`gitlab create-mr`, check whether base/target branch for PR/MR exists in remote, instead of fetching the entire remote
 
 ## New in git-machete 3.25.2
 
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 45933f4..e514378 100644
--- a/docs/man/git-machete.1
+++ b/docs/man/git-machete.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "GIT-MACHETE" "1" "Apr 25, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Apr 26, 2024" "" "git-machete"
 .SH NAME
 git-machete \- git-machete 3.25.3
 .sp
diff --git a/git_machete/client.py b/git_machete/client.py
index adc3a33..9b38746 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -2134,7 +2134,8 @@ class MacheteClient:
             else:
                 warn(f'{pr.display_text()} comes from fork and its {spec.repository_name} is already deleted. '
                      f'No remote tracking data will be set up for {bold(pr.head)} branch.')
-                self.__git.fetch_ref(org_repo_remote.remote, f'{code_hosting_client.get_ref_name_for_pull_request(pr.number)}:{pr.head}')
+                refspec = f'{code_hosting_client.get_ref_name_for_pull_request(pr.number)}:{pr.head}'
+                self.__git.fetch_refspec(org_repo_remote.remote, refspec)
                 self.__git.checkout(LocalBranchShortName.of(pr.head))
             if pr.state in ('closed', 'merged'):
                 warn(f'{pr.display_text()} is already closed.')
@@ -2552,12 +2553,17 @@ class MacheteClient:
         org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain, branch_used_for_tracking_data=head)
         code_hosting_client = spec.create_client(domain=domain, organization=org_repo_remote.organization,
                                                  repository=org_repo_remote.repository)
-        print(f"Fetching {bold(org_repo_remote.remote)}...")
-        self.__git.fetch_remote(org_repo_remote.remote)
-        base_branch_found_on_remote: bool = True
-        if '/'.join([org_repo_remote.remote, base]) not in self.__git.get_remote_branches():
-            base_branch_found_on_remote = False
-            warn(f'{spec.base_branch_name} branch for this {spec.pr_short_name} ({bold(base)}) is not found on remote, pushing...')
+
+        remote_branch = RemoteBranchShortName(f"{org_repo_remote.remote}/{base}")
+        remote_base_branch_exists_locally = remote_branch in self.__git.get_remote_branches()
+        print(f"Checking if {spec.base_branch_name} branch {bold(base)} "
+              f"exists on {bold(org_repo_remote.remote)} remote... ", end='', flush=True)
+        base_branch_found_on_remote = self.__git.does_remote_branch_exist(org_repo_remote.remote, base)
+        print(fmt('<green><b>YES</b></green>' if base_branch_found_on_remote else '<red><b>NO</b></red>'))
+        if not base_branch_found_on_remote and remote_base_branch_exists_locally:
+            self.__git.delete_remote_branch(remote_branch)
+
+        if not base_branch_found_on_remote:
             self.__handle_untracked_branch(
                 branch=base,
                 new_remote=org_repo_remote.remote,
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index d1d46c4..b13c1bc 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -347,8 +347,16 @@ class GitContext:
             self._run_git("fetch", remote, "--prune", flush_caches=True)
             self.__fetch_done_for.add(remote)
 
-    def fetch_ref(self, remote: str, ref: str) -> None:
-        self._run_git("fetch", remote, ref, flush_caches=True)
+    def fetch_refspec(self, remote: str, refspec: str) -> int:
+        return self._run_git("fetch", "--prune", remote, refspec, flush_caches=True)
+
+    def does_remote_branch_exist(self, remote: str, branch: LocalBranchShortName) -> bool:
+        # `--heads` is passed here to avoid checking for `refs/pulls/...`,
+        # which can take a lot of time in large repos (since they're present even for closed PRs).
+        # Even when a branch name or glob is passed to `git ls-remote`,
+        # data on all `refs/...` (or all `refs/heads/...`, with `--heads`) is still fetched.
+        result = self._popen_git("ls-remote", "--heads", remote, branch.full_name())
+        return result.stdout != ''
 
     def set_upstream_to(self, remote_branch: RemoteBranchShortName) -> None:
         self._run_git("branch", "--set-upstream-to", remote_branch, flush_caches=True)
@@ -962,9 +970,12 @@ class GitContext:
         return FullCommitHash.of(self._popen_git(
             "commit-tree", "HEAD^{tree}", "-p", parent_revision, "-m", msg, env=env).stdout.strip())  # noqa: FS003
 
-    def delete_branch(self, branch_name: LocalBranchShortName, force: bool = False) -> int:
+    def delete_branch(self, branch_name: LocalBranchShortName, force: bool) -> int:
         delete_option = '-D' if force else '-d'
-        return self._run_git("branch", delete_option, branch_name, flush_caches=True)
+        return self._run_git('branch', delete_option, branch_name, flush_caches=True)
+
+    def delete_remote_branch(self, branch_name: RemoteBranchShortName) -> int:
+        return self._run_git('branch', '-d', '-r', branch_name, flush_caches=True)
 
     def display_diff(self, fork_point: AnyRevision, format_with_stat: bool, branch: Optional[LocalBranchShortName] = None) -> int:
         params = []


Test patch:
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index 2db3e63..8713f2f 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -143,7 +143,7 @@ class TestGitHubCreatePR(BaseTest):
                 |
                 x-drop-constraint (untracked)
 
-            Fetching origin...
+            Checking if base branch ignore-trailing exists on origin remote... YES
             Creating a draft PR from chore/fields to ignore-trailing... OK, see www.github.com
             Checking for open GitHub PRs (to determine PR chain)... OK
             Updating description of PR #5 to include the chain of PRs... OK
@@ -290,7 +290,7 @@ class TestGitHubCreatePR(BaseTest):
               |
               o-testing/endpoints
 
-            Fetching origin...
+            Checking if base branch develop exists on origin remote... YES
             Creating a PR from allow-ownership-link to develop... OK, see www.github.com
             Setting milestone of PR #7 to 42... OK
             Adding github_user as assignee to PR #7... OK
@@ -428,8 +428,7 @@ class TestGitHubCreatePR(BaseTest):
             """
         rewrite_branch_layout_file(body)
 
-        expected_msg = ("Fetching origin...\n"
-                        "Warn: base branch for this PR (feature/api_handling) is not found on remote, pushing...\n"
+        expected_msg = ("Checking if base branch feature/api_handling exists on origin remote... NO\n"
                         "Pushing untracked branch feature/api_handling to origin...\n"
                         "Creating a PR from feature/api_exception_handling to feature/api_handling... OK, see www.github.com\n")
 
@@ -564,7 +563,7 @@ class TestGitHubCreatePR(BaseTest):
             |
             o-feature *
 
-        Fetching origin_1...
+        Checking if base branch branch-1 exists on origin_1 remote... YES
         Creating a PR from feature to branch-1... OK, see www.github.com
         Checking for open GitHub PRs (to determine PR chain)... OK
         Updating description of PR #16 to include the chain of PRs... OK
@@ -615,7 +614,7 @@ class TestGitHubCreatePR(BaseTest):
         expected_result = """
         Add feature_1 onto the inferred upstream (parent) branch feature? (y, N)
         Added branch feature_1 onto feature
-        Fetching origin_2...
+        Checking if base branch feature exists on origin_2 remote... YES
         Creating a PR from feature_1 to feature... OK, see www.github.com
         Checking for open GitHub PRs (to determine PR chain)... OK
         Updating description of PR #17 to include the chain of PRs... OK
@@ -653,7 +652,7 @@ class TestGitHubCreatePR(BaseTest):
               |
               o-feature_2 *
 
-        Fetching origin_1...
+        Checking if base branch feature exists on origin_1 remote... YES
         Creating a PR from feature_2 to feature... OK, see www.github.com
         Checking for open GitHub PRs (to determine PR chain)... OK
         Updating description of PR #18 to include the chain of PRs... OK
@@ -675,7 +674,7 @@ class TestGitHubCreatePR(BaseTest):
         expected_result = """
         Add feature_3 onto the inferred upstream (parent) branch feature_2? (y, N)
         Added branch feature_3 onto feature_2
-        Fetching origin_1...
+        Checking if base branch feature_2 exists on origin_1 remote... YES
         Creating a PR from feature_3 to feature_2... OK, see www.github.com
         Checking for open GitHub PRs (to determine PR chain)... OK
         Updating description of PR #19 to include the chain of PRs... OK
@@ -697,8 +696,7 @@ class TestGitHubCreatePR(BaseTest):
         expected_result = """
         Add feature_4 onto the inferred upstream (parent) branch feature_3? (y, N)
         Added branch feature_4 onto feature_3
-        Fetching origin_2...
-        Warn: base branch for this PR (feature_3) is not found on remote, pushing...
+        Checking if base branch feature_3 exists on origin_2 remote... NO
         Push untracked branch feature_3 to origin_2? (y, Q)
         Creating a PR from feature_4 to feature_3... OK, see www.github.com
         """
@@ -720,8 +718,7 @@ class TestGitHubCreatePR(BaseTest):
         expected_result = """
         Add feature_5 onto the inferred upstream (parent) branch feature_3? (y, N)
         Added branch feature_5 onto feature_3
-        Fetching origin...
-        Warn: base branch for this PR (feature_3) is not found on remote, pushing...
+        Checking if base branch feature_3 exists on origin remote... NO
         Push untracked branch feature_3 to origin? (y, Q)
         Creating a PR from feature_5 to feature_3... OK, see www.github.com
         """
@@ -746,7 +743,7 @@ class TestGitHubCreatePR(BaseTest):
         assert_success(
             ['github', 'create-pr'],
             """
-            Fetching origin...
+            Checking if base branch master exists on origin remote... YES
             Creating a PR from develop to master... OK, see www.github.com
             """
         )
@@ -791,7 +788,7 @@ class TestGitHubCreatePR(BaseTest):
             """
             Warn: Branch develop is behind its remote counterpart. Consider using git pull.
             Proceed with creating pull request? (y, Q)
-            Fetching origin...
+            Checking if base branch master exists on origin remote... YES
             Creating a PR from develop to master... OK, see www.github.com
             """
         )
@@ -851,7 +848,56 @@ class TestGitHubCreatePR(BaseTest):
             """
             Warn: Branch develop is diverged from and older than its remote counterpart. Consider using git reset --keep.
             Proceed with creating pull request? (y, Q)
-            Fetching origin...
+            Checking if base branch master exists on origin remote... YES
             Creating a PR from develop to master... OK, see www.github.com
             """
         )
+
+    def test_github_create_pr_when_base_branch_disappeared_from_remote(self, mocker: MockerFixture) -> 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)
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitHubAPIState()))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop").commit().push()
+            .new_branch("feature").commit().push()
+        )
+        (
+            self.repo_sandbox
+            .chdir(self.repo_sandbox.remote_path)
+            .delete_branch("develop")
+            .chdir(self.repo_sandbox.local_path)
+        )
+
+        rewrite_branch_layout_file("develop\n\tfeature")
+        self.patch_symbol(mocker, 'builtins.input', mock_input_returning('y'))
+        assert_success(
+            ['github', 'create-pr'],
+            """
+            Checking if base branch develop exists on origin remote... NO
+            Push untracked branch develop to origin? (y, Q)
+            Creating a PR from feature to develop... OK, see www.github.com
+            """
+        )
+
+    def test_github_create_pr_when_base_branch_appeared_on_remote(self, mocker: MockerFixture) -> 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)
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitHubAPIState()))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop").commit().push()
+            .delete_remote_branch("origin/develop")
+            .new_branch("feature").commit().push()
+        )
+
+        rewrite_branch_layout_file("develop\n\tfeature")
+        assert_success(
+            ['github', 'create-pr'],
+            """
+            Checking if base branch develop exists on origin remote... YES
+            Creating a PR from feature to develop... OK, see www.github.com
+            """
+        )
diff --git a/tests/test_gitlab_create_mr.py b/tests/test_gitlab_create_mr.py
index 2e8e4a6..70d3653 100644
--- a/tests/test_gitlab_create_mr.py
+++ b/tests/test_gitlab_create_mr.py
@@ -142,7 +142,7 @@ class TestGitLabCreateMR(BaseTest):
                 |
                 x-drop-constraint (untracked)
 
-            Fetching origin...
+            Checking if target branch ignore-trailing exists on origin remote... YES
             Creating a draft MR from chore/fields to ignore-trailing... OK, see www.gitlab.com
             Checking for open GitLab MRs (to determine MR chain)... OK
             Updating description of MR !5 to include the chain of MRs... OK
@@ -288,7 +288,7 @@ class TestGitLabCreateMR(BaseTest):
               |
               o-testing/endpoints
 
-            Fetching origin...
+            Checking if target branch develop exists on origin remote... YES
             Creating a MR from allow-ownership-link to develop... OK, see www.gitlab.com
             Setting milestone of MR !7 to 42... OK
             Adding gitlab_user as assignee to MR !7... OK
@@ -424,8 +424,7 @@ class TestGitLabCreateMR(BaseTest):
             """
         rewrite_branch_layout_file(body)
 
-        expected_msg = ("Fetching origin...\n"
-                        "Warn: target branch for this MR (feature/api_handling) is not found on remote, pushing...\n"
+        expected_msg = ("Checking if target branch feature/api_handling exists on origin remote... NO\n"
                         "Pushing untracked branch feature/api_handling to origin...\n"
                         "Creating a MR from feature/api_exception_handling to feature/api_handling... OK, see www.gitlab.com\n")
 
@@ -560,7 +559,7 @@ class TestGitLabCreateMR(BaseTest):
             |
             o-feature *
 
-        Fetching origin_1...
+        Checking if target branch branch-1 exists on origin_1 remote... YES
         Creating a MR from feature to branch-1... OK, see www.gitlab.com
         Checking for open GitLab MRs (to determine MR chain)... OK
         Updating description of MR !16 to include the chain of MRs... OK
@@ -611,7 +610,7 @@ class TestGitLabCreateMR(BaseTest):
         expected_result = """
         Add feature_1 onto the inferred upstream (parent) branch feature? (y, N)
         Added branch feature_1 onto feature
-        Fetching origin_2...
+        Checking if target branch feature exists on origin_2 remote... YES
         Creating a MR from feature_1 to feature... OK, see www.gitlab.com
         Checking for open GitLab MRs (to determine MR chain)... OK
         Updating description of MR !17 to include the chain of MRs... OK
@@ -649,7 +648,7 @@ class TestGitLabCreateMR(BaseTest):
               |
               o-feature_2 *
 
-        Fetching origin_1...
+        Checking if target branch feature exists on origin_1 remote... YES
         Creating a MR from feature_2 to feature... OK, see www.gitlab.com
         Checking for open GitLab MRs (to determine MR chain)... OK
         Updating description of MR !18 to include the chain of MRs... OK
@@ -671,7 +670,7 @@ class TestGitLabCreateMR(BaseTest):
         expected_result = """
         Add feature_3 onto the inferred upstream (parent) branch feature_2? (y, N)
         Added branch feature_3 onto feature_2
-        Fetching origin_1...
+        Checking if target branch feature_2 exists on origin_1 remote... YES
         Creating a MR from feature_3 to feature_2... OK, see www.gitlab.com
         Checking for open GitLab MRs (to determine MR chain)... OK
         Updating description of MR !19 to include the chain of MRs... OK
@@ -693,8 +692,7 @@ class TestGitLabCreateMR(BaseTest):
         expected_result = """
         Add feature_4 onto the inferred upstream (parent) branch feature_3? (y, N)
         Added branch feature_4 onto feature_3
-        Fetching origin_2...
-        Warn: target branch for this MR (feature_3) is not found on remote, pushing...
+        Checking if target branch feature_3 exists on origin_2 remote... NO
         Push untracked branch feature_3 to origin_2? (y, Q)
         Creating a MR from feature_4 to feature_3... OK, see www.gitlab.com
         """
@@ -716,8 +714,7 @@ class TestGitLabCreateMR(BaseTest):
         expected_result = """
         Add feature_5 onto the inferred upstream (parent) branch feature_3? (y, N)
         Added branch feature_5 onto feature_3
-        Fetching origin...
-        Warn: target branch for this MR (feature_3) is not found on remote, pushing...
+        Checking if target branch feature_3 exists on origin remote... NO
         Push untracked branch feature_3 to origin? (y, Q)
         Creating a MR from feature_5 to feature_3... OK, see www.gitlab.com
         """
@@ -742,7 +739,7 @@ class TestGitLabCreateMR(BaseTest):
         assert_success(
             ['gitlab', 'create-mr'],
             """
-            Fetching origin...
+            Checking if target branch master exists on origin remote... YES
             Creating a MR from develop to master... OK, see www.gitlab.com
             """
         )
@@ -787,7 +784,7 @@ class TestGitLabCreateMR(BaseTest):
             """
             Warn: Branch develop is behind its remote counterpart. Consider using git pull.
             Proceed with creating merge request? (y, Q)
-            Fetching origin...
+            Checking if target branch master exists on origin remote... YES
             Creating a MR from develop to master... OK, see www.gitlab.com
             """
         )
@@ -847,7 +844,56 @@ class TestGitLabCreateMR(BaseTest):
             """
             Warn: Branch develop is diverged from and older than its remote counterpart. Consider using git reset --keep.
             Proceed with creating merge request? (y, Q)
-            Fetching origin...
+            Checking if target branch master exists on origin remote... YES
             Creating a MR from develop to master... OK, see www.gitlab.com
             """
         )
+
+    def test_gitlab_create_mr_when_target_branch_disappeared_from_remote(self, mocker: MockerFixture) -> 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)
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitLabAPIState()))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop").commit().push()
+            .new_branch("feature").commit().push()
+        )
+        (
+            self.repo_sandbox
+            .chdir(self.repo_sandbox.remote_path)
+            .delete_branch("develop")
+            .chdir(self.repo_sandbox.local_path)
+        )
+
+        rewrite_branch_layout_file("develop\n\tfeature")
+        self.patch_symbol(mocker, 'builtins.input', mock_input_returning('y'))
+        assert_success(
+            ['gitlab', 'create-mr'],
+            """
+            Checking if target branch develop exists on origin remote... NO
+            Push untracked branch develop to origin? (y, Q)
+            Creating a MR from feature to develop... OK, see www.gitlab.com
+            """
+        )
+
+    def test_gitlab_create_mr_when_target_branch_appeared_on_remote(self, mocker: MockerFixture) -> 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)
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitLabAPIState()))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop").commit().push()
+            .delete_remote_branch("origin/develop")
+            .new_branch("feature").commit().push()
+        )
+
+        rewrite_branch_layout_file("develop\n\tfeature")
+        assert_success(
+            ['gitlab', 'create-mr'],
+            """
+            Checking if target branch develop exists on origin remote... YES
+            Creating a MR from feature to develop... OK, see www.gitlab.com
+            """
+        )