Repository: VirtusLab/git-machete
Pull request #580 (2022-07-14 08:47:05)

Release v3.11.2


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index be28424..524a45e 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,10 @@
 # Release notes
 
+## New in git-machete 3.11.2
+
+- fixed: `git machete` now correctly infers remote for fetching of branch when the branch is associated with more than one remote
+- fixed: `git machete github create-pr` and `retarget-pr` now take branch tracking data into account when finding out where (in what GitHub organization/repository) to create a PR
+
 ## New in git-machete 3.11.1
 
 - fixed: release to Snap Store
diff --git a/ci/checks/prohibit-trailing-whitespace.sh b/ci/checks/prohibit-trailing-whitespace.sh
index 0d46abd..e67e9ae 100755
--- a/ci/checks/prohibit-trailing-whitespace.sh
+++ b/ci/checks/prohibit-trailing-whitespace.sh
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-if git grep -EIn '\s+$'; then
+if git grep -EIn '\s+$' -- :^'*.py'; then
   echo 'The above lines contain trailing whitespace, please tidy up'
   exit 1
 fi
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 94015dc..669870b 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.11.1'
+__version__ = '3.11.2'
diff --git a/git_machete/client.py b/git_machete/client.py
index 2708312..1e38f48 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -949,7 +949,7 @@ class MacheteClient:
         next_sibling_of_ancestor: List[Optional[LocalBranchShortName]]
         for branch, next_sibling_of_ancestor in next_sibling_of_ancestor_by_branch:
             if branch in self.up_branch:
-                print_line_prefix(branch, f"{utils.get_vertical_bar()} \n")
+                print_line_prefix(branch, f"{utils.get_vertical_bar()}\n")
                 if opt_list_commits:
                     if not fp_hash(branch):
                         # Rare case, but can happen e.g. due to reflog expiry.
@@ -2012,7 +2012,7 @@ class MacheteClient:
     def retarget_github_pr(self, head: LocalBranchShortName) -> None:
         org: str
         repo: str
-        _, org, repo = self.__derive_remote_and_github_org_and_repo()
+        _, org, repo = self.__derive_remote_and_github_org_and_repo(branch_used_for_tracking_data=head)
 
         debug(f'organization is {org}, repository is {repo}')
 
@@ -2035,7 +2035,7 @@ class MacheteClient:
         else:
             print(fmt(f'The base branch of PR #{pr.number} is already `{new_base}`'))
 
-    def __derive_remote_and_github_org_and_repo(self) -> RemoteAndOrganizationAndRepository:
+    def __derive_remote_and_github_org_and_repo(self, branch_used_for_tracking_data: LocalBranchShortName = None) -> RemoteAndOrganizationAndRepository:
         remote_and_organization_and_repository_for_custom_url = self.__get_remote_and_organization_and_repository_name_for_custom_url()
         if all(remote_and_organization_and_repository_for_custom_url):
             return remote_and_organization_and_repository_for_custom_url
@@ -2049,6 +2049,7 @@ class MacheteClient:
         remote_and_github_org_and_repo: Dict[str, RemoteAndOrganizationAndRepository] = {
             remote: get_parsed_github_remote_url(url, remote) for remote, url in url_for_remote.items() if is_github_remote_url(url)
         }
+
         if not remote_and_github_org_and_repo:
             raise MacheteException(
                 fmt('Remotes are defined for this repository, but none of them '
@@ -2063,9 +2064,16 @@ class MacheteClient:
         if 'origin' in remote_and_github_org_and_repo:
             return remote_and_github_org_and_repo['origin']
 
+        if len(remote_and_github_org_and_repo) > 1 and branch_used_for_tracking_data is not None:
+            remote_for_fetching_of_branch = self.__git.get_combined_remote_for_fetching_of_branch(
+                branch=branch_used_for_tracking_data,
+                remotes=list(remote_and_github_org_and_repo.keys()))
+            if remote_for_fetching_of_branch is not None:
+                return remote_and_github_org_and_repo[remote_for_fetching_of_branch]
+
         raise MacheteException(
             f'Multiple non-origin remotes correspond to GitHub in this repository: '
-            f'{", ".join(remote_and_github_org_and_repo.keys())}, aborting. \n'
+            f'{", ".join(remote_and_github_org_and_repo.keys())} -> aborting. \n'
             f'You can also select the repository by providing 3 git config keys: '
             '`machete.github.{remote,organization,repository}`\n')
 
@@ -2075,7 +2083,12 @@ class MacheteClient:
                                                   repository=self.__git.get_config_attr_or_none("machete.github.repository"))
 
     def create_github_pr(
-            self, *, head: LocalBranchShortName, opt_draft: bool, opt_onto: Optional[LocalBranchShortName]) -> None:
+            self,
+            *,
+            head: LocalBranchShortName,
+            opt_draft: bool,
+            opt_onto: Optional[LocalBranchShortName]
+    ) -> None:
         # first make sure that head branch is synced with remote
         self.__sync_before_creating_pr(opt_onto=opt_onto, opt_yes=False)
         self.flush_caches()
@@ -2085,7 +2098,7 @@ class MacheteClient:
             raise MacheteException(f'Could not determine base branch for PR. Branch `{head}` is a root branch.')
         org: str
         repo: str
-        remote, org, repo = self.__derive_remote_and_github_org_and_repo()
+        remote, org, repo = self.__derive_remote_and_github_org_and_repo(branch_used_for_tracking_data=head)
         print(f"Fetching {remote}...")
         self.__git.fetch_remote(remote)
         if '/'.join([remote, base]) not in self.__git.get_remote_branches():
@@ -2110,7 +2123,7 @@ class MacheteClient:
         description_path = self.__git.get_main_git_subpath('info', 'description')
         description: str = utils.slurp_file_or_empty(description_path)
 
-        ok_str = ' <green>-> <b>OK</b></green>'
+        ok_str = ' <green><b>OK</b></green>'
         print(fmt(f'Creating a {"draft " if opt_draft else ""}PR from `{head}` to `{base}`...'), end='', flush=True)
         pr: GitHubPullRequest = create_pull_request(org, repo, head=head, base=base, title=commits[0].subject,
                                                     description=description, draft=opt_draft)
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index d4904cc..2aff93d 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -435,25 +435,38 @@ class GitContext:
             self.__load_branches()
         return self.__committer_unix_timestamp_by_revision_cached.get(revision.full_name(), 0)
 
-    def get_inferred_remote_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[str]:
-        # Since many people don't use '--set-upstream' flag of 'push', we try to infer the remote instead.
-        for remote in self.get_remotes():
-            if f"{remote}/{branch}" in self.get_remote_branches():
-                return remote
-        return None
+    def __get_remotes_containing_branch(self, branch: LocalBranchShortName, remotes: Optional[List[str]] = None) -> List[str]:
+        remotes = remotes if remotes else self.get_remotes()
+        remote_branches = self.get_remote_branches()
+        return [remote for remote in remotes if f'{remote}/{branch}' in remote_branches]
+
+    def get_inferred_remote_for_fetching_of_branch(self, branch: LocalBranchShortName, remotes: Optional[List[str]] = None) -> Optional[str]:
+        remotes_containing_branch: List[str] = self.__get_remotes_containing_branch(branch=branch, remotes=remotes)
+        if len(remotes_containing_branch) > 1 or len(remotes_containing_branch) == 0:
+            debug(f'Can\'t infer remote for fetching of branch.\n'
+                  f'There are {len(remotes_containing_branch)} remotes: {", ".join(remotes_containing_branch)} '
+                  f'containing {branch} branch.')
+            return None
+        else:
+            return remotes_containing_branch[0]
 
     def get_strict_remote_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[str]:
         remote = self.get_config_attr_or_none(f"branch.{branch}.remote")
         return remote.rstrip() if remote else None
 
-    def get_combined_remote_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[str]:
-        return self.get_strict_remote_for_fetching_of_branch(branch) or self.get_inferred_remote_for_fetching_of_branch(branch)
+    def get_combined_remote_for_fetching_of_branch(self, branch: LocalBranchShortName, remotes: Optional[List[str]] = None) -> Optional[str]:
+        # Since many people don't use '--set-upstream' flag of 'push', we try to infer the remote instead if the tracking data is missing.
+        return self.get_strict_remote_for_fetching_of_branch(branch) or self.get_inferred_remote_for_fetching_of_branch(branch, remotes)
 
     def __get_inferred_counterpart_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[RemoteBranchShortName]:
-        for remote in self.get_remotes():
-            if f"{remote}/{branch}" in self.get_remote_branches():
-                return RemoteBranchShortName.of(f"{remote}/{branch}")
-        return None
+        remotes_containing_branch: List[str] = self.__get_remotes_containing_branch(branch)
+        if len(remotes_containing_branch) > 1 or len(remotes_containing_branch) == 0:
+            debug(f'Can\'t infer local branch\'s remote counterpart for fetching of branch.\n'
+                  f'There are {len(remotes_containing_branch)} remotes: {", ".join(remotes_containing_branch)} '
+                  f'containing branch {branch}.')
+            return None
+        else:
+            return RemoteBranchShortName.of(f"{remotes_containing_branch[0]}/{branch}")
 
     def get_strict_counterpart_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[RemoteBranchShortName]:
         if self.__counterparts_for_fetching_cached is None:


Test patch:
diff --git a/tests/mockers.py b/tests/mockers.py
index 5a03075..fdab93d 100644
--- a/tests/mockers.py
+++ b/tests/mockers.py
@@ -55,10 +55,13 @@ class GitRepositorySandbox:
         subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
         return self
 
-    def new_repo(self, *args: str) -> "GitRepositorySandbox":
+    def new_repo(self, *args: str, switch_dir_to_new_repo: bool = True) -> "GitRepositorySandbox":
+        previous_dir = os.getcwd()
         os.chdir(args[0])
         opts = args[1:]
         self.execute(f"git init {' '.join(opts)}")
+        if not switch_dir_to_new_repo:
+            os.chdir(previous_dir)
         return self
 
     def new_branch(self, branch_name: str) -> "GitRepositorySandbox":
@@ -90,9 +93,9 @@ class GitRepositorySandbox:
         self.execute(f'git commit --amend -m "{message}"')
         return self
 
-    def push(self) -> "GitRepositorySandbox":
+    def push(self, remote: str = 'origin', set_upstream: bool = True) -> "GitRepositorySandbox":
         branch = popen("git symbolic-ref -q --short HEAD")
-        self.execute(f"git push -u origin {branch}")
+        self.execute(f"git push {'--set-upstream' if set_upstream else ''} {remote} {branch}")
         return self
 
     def sleep(self, seconds: int) -> "GitRepositorySandbox":
@@ -331,8 +334,8 @@ class MockContextManager:
         pass
 
 
-def adapt(s: str) -> str:
-    return textwrap.indent(textwrap.dedent(re.sub(r"\|\n", "| \n", s[1:])), "  ")
+def adapt(s: str, indent: str) -> str:
+    return textwrap.indent(textwrap.dedent(s[1:]), indent)
 
 
 def launch_command(*args: str) -> str:
@@ -344,8 +347,9 @@ def launch_command(*args: str) -> str:
         return out.getvalue()
 
 
-def assert_command(cmds: Iterable[str], expected_result: str, strip_indentation: bool = True) -> None:
-    assert launch_command(*cmds) == (adapt(expected_result) if strip_indentation else expected_result)
+def assert_command(cmds: Iterable[str], expected_result: str, strip_indentation: bool = True, indent: str = '  ') -> None:
+    expected_result = adapt(expected_result, indent) if strip_indentation else expected_result
+    assert launch_command(*cmds) == expected_result
 
 
 def rewrite_definition_file(new_body: str) -> None:
diff --git a/tests/test_github.py b/tests/test_github.py
index 71d0439..a3a7cf5 100644
--- a/tests/test_github.py
+++ b/tests/test_github.py
@@ -1,5 +1,6 @@
 import os
 import subprocess
+from tempfile import mkdtemp
 from typing import Any, Dict, Optional
 from unittest import mock
 
@@ -44,6 +45,11 @@ def mock__get_github_token_fake() -> Optional[str]:
     return 'token'
 
 
+def mock_input(msg: str) -> str:
+    print(msg)
+    return '1'
+
+
 class TestGithub:
     mock_repository_info: Dict[str, str] = {'full_name': 'testing/checkout_prs',
                                             'html_url': 'https://github.com/tester/repo_sandbox.git'}
@@ -69,6 +75,24 @@ class TestGithub:
                 'user': {'login': 'github_user'},
                 'base': {'ref': 'root'}, 'number': '15',
                 'html_url': 'www.github.com', 'state': 'open'
+            },
+            {
+                'head': {'ref': 'feature_1', 'repo': mock_repository_info},
+                'user': {'login': 'github_user'},
+                'base': {'ref': 'root'}, 'number': '20',
+                'html_url': 'www.github.com', 'state': 'open'
+            },
+            {
+                'head': {'ref': 'feature_2', 'repo': mock_repository_info},
+                'user': {'login': 'github_user'},
+                'base': {'ref': 'root'}, 'number': '25',
+                'html_url': 'www.github.com', 'state': 'open'
+            },
+            {
+                'head': {'ref': 'feature_3', 'repo': mock_repository_info},
+                'user': {'login': 'github_user'},
+                'base': {'ref': 'root'}, 'number': '35',
+                'html_url': 'www.github.com', 'state': 'open'
             }
         ]
     )
@@ -105,6 +129,108 @@ class TestGithub:
             strip_indentation=False
         )
 
+    @mock.patch('git_machete.cli.exit_script', mock_exit_script)
+    @mock.patch('git_machete.github.GITHUB_REMOTE_PATTERNS', FAKE_GITHUB_REMOTE_PATTERNS)
+    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+    @mock.patch('urllib.request.Request', git_api_state_for_test_retarget_pr.new_request())
+    @mock.patch('urllib.request.urlopen', MockContextManager)
+    def test_github_retarget_pr_multiple_non_origin_remotes(self) -> None:
+        branchs_first_commit_msg = "First commit on branch."
+        branchs_second_commit_msg = "Second commit on branch."
+
+        origin_1_remote_path = mkdtemp()
+        origin_2_remote_path = mkdtemp()
+        self.repo_sandbox.new_repo(origin_1_remote_path, switch_dir_to_new_repo=False)
+        self.repo_sandbox.new_repo(origin_2_remote_path, switch_dir_to_new_repo=False)
+
+        # branch feature present in each remote, no branch tracking data
+        (
+            self.repo_sandbox.remove_remote(remote='origin')
+                .new_branch("root")
+                .add_remote('origin_1', origin_1_remote_path)
+                .add_remote('origin_2', origin_2_remote_path)
+                .commit("First commit on root.")
+                .push(remote='origin_1')
+                .push(remote='origin_2')
+                .new_branch("branch-1")
+                .commit(branchs_first_commit_msg)
+                .commit(branchs_second_commit_msg)
+                .push(remote='origin_1')
+                .push(remote='origin_2')
+                .new_branch('feature')
+                .commit('introduce feature')
+                .push(remote='origin_1', set_upstream=False)
+                .push(remote='origin_2', set_upstream=False)
+        )
+
+        launch_command("discover", "-y")
+        expected_error_message = (
+            "Multiple non-origin remotes correspond to GitHub in this repository: origin_1, origin_2 -> aborting. \n"
+            "You can also select the repository by providing 3 git config keys: `machete.github.{remote,organization,repository}`\n"
+        )
+        with pytest.raises(MacheteException) as e:
+            launch_command("github", "retarget-pr")
+        if e:
+            assert e.value.args[0] == expected_error_message, \
+                'Verify that expected error message has appeared when given pull request to create is already created.'
+
+        # branch feature_1 present in each remote, tracking data present
+        (
+            self.repo_sandbox.check_out('feature')
+                .new_branch('feature_1')
+                .commit('introduce feature 1')
+                .push(remote='origin_1')
+                .push(remote='origin_2')
+        )
+
+        launch_command("discover", "-y")
+        assert_command(
+            ['github', 'retarget-pr'],
+            'The base branch of PR #20 has been switched to `feature`\n',
+            strip_indentation=False
+        )
+
+        # branch feature_2 is not present in any of the remotes
+        (
+            self.repo_sandbox.check_out('feature')
+                .new_branch('feature_2')
+                .commit('introduce feature 2')
+        )
+
+        launch_command("discover", "-y")
+        with pytest.raises(MacheteException) as e:
+            launch_command("github", "retarget-pr")
+        if e:
+            assert e.value.args[0] == expected_error_message, \
+                'Verify that expected error message has appeared when given pull request to create is already created.'
+
+        # branch feature_2 present in only one remote: origin_1 and there is no tracking data available -> infer the remote
+        (
+            self.repo_sandbox.check_out('feature_2')
+                .push(remote='origin_1', set_upstream=False)
+        )
+
+        assert_command(
+            ['github', 'retarget-pr'],
+            'The base branch of PR #25 has been switched to `feature`\n',
+            strip_indentation=False
+        )
+
+        # branch feature_3 present in only one remote: origin_1 and has tracking data
+        (
+            self.repo_sandbox.check_out('feature_2')
+                .new_branch('feature_3')
+                .commit('introduce feature 3')
+                .push(remote='origin_1')
+        )
+
+        launch_command("discover", "-y")
+        assert_command(
+            ['github', 'retarget-pr'],
+            'The base branch of PR #35 has been switched to `feature_2`\n',
+            strip_indentation=False
+        )
+
     git_api_state_for_test_anno_prs = MockGitHubAPIState(
         [
             {
@@ -454,7 +580,7 @@ class TestGithub:
 
         expected_msg = ("Fetching origin...\n"
                         "Warn: Base branch for this PR (`feature/api_handling`) is not found on remote, pushing...\n"
-                        "Creating a PR from `feature/api_exception_handling` to `feature/api_handling`... -> OK, see www.github.com\n")
+                        "Creating a PR from `feature/api_exception_handling` to `feature/api_handling`... OK, see www.github.com\n")
         assert_command(['github', 'create-pr'], expected_msg, strip_indentation=False)
         assert_command(
             ['status'],
@@ -467,6 +593,208 @@ class TestGithub:
             """,
         )
 
+    git_api_state_for_test_github_create_pr_with_multiple_non_origin_remotes = MockGitHubAPIState(
+        [
+            {
+                'head': {'ref': 'branch-1', 'repo': mock_repository_info},
+                'user': {'login': 'github_user'},
+                'base': {'ref': 'root'}, 'number': '15',
+                'html_url': 'www.github.com', 'state': 'open'
+            }
+        ],
+        issues=[
+            {'number': '16'},
+            {'number': '17'},
+            {'number': '18'},
+            {'number': '19'},
+            {'number': '20'},
+        ]
+    )
+
+    @mock.patch('git_machete.cli.exit_script', mock_exit_script)
+    @mock.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
+    # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_create_pr` due to `git fetch` executed by `create-pr` subcommand.
+    @mock.patch('git_machete.github.GITHUB_REMOTE_PATTERNS', FAKE_GITHUB_REMOTE_PATTERNS)
+    @mock.patch('git_machete.github.__get_github_token', mock__get_github_token)
+    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+    @mock.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+    @mock.patch('urllib.error.HTTPError', MockHTTPError)  # need to provide read() method, which does not actually reads error from url
+    @mock.patch('urllib.request.Request', git_api_state_for_test_github_create_pr_with_multiple_non_origin_remotes.new_request())
+    @mock.patch('urllib.request.urlopen', MockContextManager)
+    @mock.patch('builtins.input', mock_input)
+    def test_github_create_pr_with_multiple_non_origin_remotes(self) -> None:
+        origin_1_remote_path = mkdtemp()
+        origin_2_remote_path = mkdtemp()
+        self.repo_sandbox.new_repo(origin_1_remote_path, "--bare", switch_dir_to_new_repo=False)
+        self.repo_sandbox.new_repo(origin_2_remote_path, "--bare", switch_dir_to_new_repo=False)
+
+        # branch feature present in each of the remotes, no branch tracking data, remote origin_1 picked manually via mock_input()
+        (
+            self.repo_sandbox.remove_remote(remote='origin')
+                .new_branch("root")
+                .add_remote('origin_1', origin_1_remote_path)
+                .add_remote('origin_2', origin_2_remote_path)
+                .commit("First commit on root.")
+                .push(remote='origin_1')
+                .push(remote='origin_2')
+                .new_branch("branch-1")
+                .commit('First commit on branch-1.')
+                .push(remote='origin_1')
+                .push(remote='origin_2')
+                .new_branch('feature')
+                .commit('introduce feature')
+                .push(remote='origin_1', set_upstream=False)
+                .push(remote='origin_2', set_upstream=False)
+        )
+
+        launch_command("discover", "-y")
+        expected_result = """
+        Branch `feature` is untracked and there's no `origin` repository.
+        [1] origin_1
+        [2] origin_2
+        Select number 1..2 to specify the destination remote repository, or 'q' to quit creating pull request: 
+        Branch feature is untracked, but its remote counterpart candidate origin_1/feature already exists and both branches point to the same commit.
+
+          root
+          |
+          o-branch-1
+            |
+            o-feature *
+
+        Fetching origin_1...
+        Creating a PR from `feature` to `branch-1`... OK, see www.github.com
+        """  # noqa: W291
+        assert_command(
+            ['github', 'create-pr'],
+            expected_result,
+            indent=''
+        )
+        # branch feature_1 present in each of the remotes, tracking data present
+        (
+            self.repo_sandbox.check_out('feature')
+                .new_branch('feature_1')
+                .commit('introduce feature 1')
+                .push(remote='origin_1')
+                .push(remote='origin_2')
+        )
+
+        expected_result = """
+        Added branch `feature_1` onto `feature`
+        Fetching origin_2...
+        Creating a PR from `feature_1` to `feature`... OK, see www.github.com
+        """
+        assert_command(
+            ['github', 'create-pr'],
+            expected_result,
+            indent=''
+        )
+
+        # branch feature_2 not present in any of the remotes, remote origin_1 picked manually via mock_input()
+        (
+            self.repo_sandbox.check_out('feature')
+                .new_branch('feature_2')
+                .commit('introduce feature 2')
+        )
+
+        expected_result = """
+        Added branch `feature_2` onto `feature`
+        Branch `feature_2` is untracked and there's no `origin` repository.
+        [1] origin_1
+        [2] origin_2
+        Select number 1..2 to specify the destination remote repository, or 'q' to quit creating pull request: 
+
+          root
+          |
+          o-branch-1
+            |
+            o-feature  PR #16
+              |
+              o-feature_1  PR #17
+              |
+              o-feature_2 *
+
+        Fetching origin_1...
+        Creating a PR from `feature_2` to `feature`... OK, see www.github.com
+        """  # noqa: W291
+        assert_command(
+            ['github', 'create-pr'],
+            expected_result,
+            indent=''
+        )
+
+        # branch feature_2 present in only one remote: origin_1, no tracking data
+        (
+            self.repo_sandbox.check_out('feature_2')
+                .new_branch('feature_3')
+                .commit('introduce feature 3')
+                .push(remote='origin_1', set_upstream=False)
+        )
+
+        expected_result = """
+        Added branch `feature_3` onto `feature_2`
+        Branch feature_3 is untracked, but its remote counterpart candidate origin_1/feature_3 already exists and both branches point to the same commit.
+
+          root
+          |
+          o-branch-1
+            |
+            o-feature  PR #16
+              |
+              o-feature_1  PR #17
+              |
+              o-feature_2  PR #18
+                |
+                o-feature_3 *
+
+        Fetching origin_1...
+        Creating a PR from `feature_3` to `feature_2`... OK, see www.github.com
+        """
+        assert_command(
+            ['github', 'create-pr'],
+            expected_result,
+            indent=''
+        )
+
+        # branch feature_3 present in only one remote: origin_2, tracking data present
+        (
+            self.repo_sandbox.check_out('feature_3')
+                .new_branch('feature_4')
+                .commit('introduce feature 4')
+                .push(remote='origin_2')
+        )
+
+        expected_result = """
+        Added branch `feature_4` onto `feature_3`
+        Fetching origin_2...
+        Warn: Base branch for this PR (`feature_3`) is not found on remote, pushing...
+        Creating a PR from `feature_4` to `feature_3`... OK, see www.github.com
+        """
+        assert_command(
+            ['github', 'create-pr'],
+            expected_result,
+            indent=''
+        )
+
+        # branch feature_3 present in only one remote: origin_2 with tracking data, origin remote present - takes priority
+        (
+            self.repo_sandbox.add_remote('origin', self.repo_sandbox.remote_path)
+                .check_out('feature_3')
+                .new_branch('feature_5')
+                .commit('introduce feature 5')
+                .push(remote='origin_2')
+        )
+
+        expected_result = """
+        Added branch `feature_5` onto `feature_3`
+        Fetching origin...
+        Creating a PR from `feature_5` to `feature_3`... OK, see www.github.com
+        """
+        assert_command(
+            ['github', 'create-pr'],
+            expected_result,
+            indent=''
+        )
+
     git_api_state_for_test_checkout_prs = MockGitHubAPIState(
         [
             {
diff --git a/tests/test_status.py b/tests/test_status.py
index a507322..5041710 100644
--- a/tests/test_status.py
+++ b/tests/test_status.py
@@ -1,3 +1,5 @@
+import os
+from tempfile import mkdtemp
 from typing import Any
 
 import pytest
@@ -67,24 +69,26 @@ class TestStatus:
         launch_command('discover', '-y')
 
         expected_status_output = (
-"""   master
-   |
-   o- bar
-      |
-      o- foo *
-"""  # noqa: E122
+            """
+            master
+            |
+            o- bar
+               |
+               o- foo *
+            """
         )
-        assert_command(['status'], expected_status_output.replace('|', '| ', 2), strip_indentation=False)
+        assert_command(['status'], expected_status_output, indent='   ')
 
         self.repo_sandbox.add_git_config_key('machete.status.extraSpaceBeforeBranchName', 'false')
 
         expected_status_output = (
-"""  master
- |
- o-bar
-   |
-   o-foo *
-"""  # noqa: E122
+            """
+            master
+            |
+            o-bar
+              |
+              o-foo *
+            """
         )
         assert_command(['status'], expected_status_output)
 
@@ -232,3 +236,48 @@ class TestStatus:
             o-develop *
             """,
         )
+
+    def test_inferring_counterpart_for_fetching_of_branch(self, mocker: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+
+        origin_1_remote_path = mkdtemp()
+        self.repo_sandbox.new_repo(origin_1_remote_path)
+
+        os.chdir(self.repo_sandbox.local_path)
+
+        (
+            self.repo_sandbox
+                .add_remote('origin_1', origin_1_remote_path)
+                .new_branch('master')
+                .commit()
+                .push()
+                .new_branch('bar')
+                .commit()
+                .push()
+                .new_branch('foo')
+                .commit()
+                .push(set_upstream=False)
+                .push(remote='origin_1', set_upstream=False)
+                .new_branch('snickers')
+                .commit()
+                .push(remote='origin_1', set_upstream=False)
+                .new_branch('mars')
+                .commit()
+                .push()
+                .push(remote='origin_1')
+        )
+        launch_command('discover', '-y')
+        expected_status_output = (
+"""  master
+  |
+  o-bar
+    |
+    o-foo (untracked)
+      |
+      o-snickers
+        |
+        o-mars *
+"""  # noqa: E122
+        )
+        assert_command(['status'], expected_status_output, strip_indentation=False)