Repository: VirtusLab/git-machete
Pull request #1028 (2023-08-14 18:56:35)

Improve semantics of `machete.github.*` git config keys

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1028?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) Report
> Merging [#1028](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1028?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) (9511f3e) into [develop](https://app.codecov.io/gh/VirtusLab/git-machete/commit/573afd98355ffbb35085b10d32448455d22c8b22?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) (573afd9) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff            @@
##           develop    #1028   +/-   ##
========================================
  Coverage    98.51%   98.52%           
========================================
  Files           12       12           
  Lines         3163     3177   +14     
  Branches       707      715    +8     
========================================
+ Hits          3116     3130   +14     
  Misses          29       29           
  Partials        18       18           
```


| [Files Changed](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1028?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) | Coverage Δ | |
|---|---|---|
| [git\_machete/generated\_docs.py](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1028?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab#diff-Z2l0X21hY2hldGUvZ2VuZXJhdGVkX2RvY3MucHk=) | `100.00% <ø> (ø)` | |
| [git\_machete/client.py](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1028?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab#diff-Z2l0X21hY2hldGUvY2xpZW50LnB5) | `99.38% <100.00%> (+<0.01%)` | :arrow_up: |
| [git\_machete/github.py](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1028?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab#diff-Z2l0X21hY2hldGUvZ2l0aHViLnB5) | `98.32% <100.00%> (-0.03%)` | :arrow_down: |



Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index acbf7fb..f30012a 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,6 +2,8 @@
 
 ## New in git-machete 3.18.1
 
+- fixed: `machete.github.remote` git config key can be specified independently from `machete.github.organization` and `machete.github.repository`
+
 ## New in git-machete 3.18.0
 
 - added: `git machete completion bash|fish|zsh` command
diff --git a/ci/checks/enforce-mocking-only-whitelisted-methods.sh b/ci/checks/enforce-mocking-only-whitelisted-methods.sh
index 7abeee3..9e179a4 100755
--- a/ci/checks/enforce-mocking-only-whitelisted-methods.sh
+++ b/ci/checks/enforce-mocking-only-whitelisted-methods.sh
@@ -13,7 +13,7 @@ git_machete.client.MacheteClient.is_stdout_a_tty
 git_machete.git_operations.GitContext.fetch_remote
 git_machete.github.GitHubClient.MAX_PULLS_PER_PAGE_COUNT
 git_machete.github.GitHubToken.for_domain
-git_machete.github.RemoteAndOrganizationAndRepository.from_url
+git_machete.github.OrganizationAndRepository.from_url
 git_machete.utils._popen_cmd
 git_machete.utils._run_cmd
 git_machete.utils.find_executable
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 18ea7e3..07e1d67 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" "Aug 14, 2023" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Aug 15, 2023" "" "git-machete"
 .SH NAME
 git-machete \- git-machete 3.18.1
 .sp
@@ -342,20 +342,18 @@ You can alter the default behavior by setting the following git config keys:
 .B GitHub Enterprise domain
 E.g. \fBgit config machete.github.domain git.example.org\fP
 .TP
-.B Remote name
+.B Remote name (as in \fBgit remote\fP)
 E.g. \fBgit config machete.github.remote origin\fP
 .TP
-.B Organization name
-E.g. \fBgit config machete.github.organization VirtusLab\fP
-.TP
-.B Repository name
-E.g. \fBgit config machete.github.repository git\-machete\fP
+.B Organization and repository name
+E.g. \fBgit config machete.github.organization VirtusLab; git config machete.github.repository git\-machete\fP
 .UNINDENT
 .UNINDENT
 .UNINDENT
 .sp
 Note that you do \fBnot\fP need to set all four keys at once.
 For example, in a typical usage of GitHub Enterprise, it should be enough to just set \fBmachete.github.domain\fP\&.
+Only \fBmachete.github.organization\fP and \fBmachete.github.repository\fP must be specified together.
 .UNINDENT
 .UNINDENT
 .sp
@@ -404,25 +402,23 @@ You can alter the default behavior by setting the following git config keys:
 .B GitHub Enterprise domain
 E.g. \fBgit config machete.github.domain git.example.org\fP
 .TP
-.B Remote name
+.B Remote name (as in \fBgit remote\fP)
 E.g. \fBgit config machete.github.remote origin\fP
 .TP
-.B Organization name
-E.g. \fBgit config machete.github.organization VirtusLab\fP
-.TP
-.B Repository name
-E.g. \fBgit config machete.github.repository git\-machete\fP
+.B Organization and repository name
+E.g. \fBgit config machete.github.organization VirtusLab; git config machete.github.repository git\-machete\fP
 .UNINDENT
 .UNINDENT
 .UNINDENT
 .sp
 Note that you do \fBnot\fP need to set all four keys at once.
 For example, in a typical usage of GitHub Enterprise, it should be enough to just set \fBmachete.github.domain\fP\&.
+Only \fBmachete.github.organization\fP and \fBmachete.github.repository\fP must be specified together.
 .UNINDENT
 .UNINDENT
 .TP
 .B \fBmachete.overrideForkPoint.<branch>.to\fP:
-Executing \fBgit machete fork\-point \-\-override\-to[\-parent|\-inferred|=<revision>] [<branch>]\fP sets up a fork point override for <branch>.
+Executing \fBgit machete fork\-point \-\-override\-to[\-parent|\-inferred|=<revision>] [<branch>]\fP sets up a fork point override for \fB<branch>\fP\&.
 .sp
 The override data is stored under \fBmachete.overrideForkPoint.<branch>.to\fP git config key.
 .sp
@@ -592,20 +588,18 @@ You can alter the default behavior by setting the following git config keys:
 .B GitHub Enterprise domain
 E.g. \fBgit config machete.github.domain git.example.org\fP
 .TP
-.B Remote name
+.B Remote name (as in \fBgit remote\fP)
 E.g. \fBgit config machete.github.remote origin\fP
 .TP
-.B Organization name
-E.g. \fBgit config machete.github.organization VirtusLab\fP
-.TP
-.B Repository name
-E.g. \fBgit config machete.github.repository git\-machete\fP
+.B Organization and repository name
+E.g. \fBgit config machete.github.organization VirtusLab; git config machete.github.repository git\-machete\fP
 .UNINDENT
 .UNINDENT
 .UNINDENT
 .sp
 Note that you do \fBnot\fP need to set all four keys at once.
 For example, in a typical usage of GitHub Enterprise, it should be enough to just set \fBmachete.github.domain\fP\&.
+Only \fBmachete.github.organization\fP and \fBmachete.github.repository\fP must be specified together.
 .UNINDENT
 .UNINDENT
 .sp
@@ -1060,20 +1054,18 @@ You can alter the default behavior by setting the following git config keys:
 .B GitHub Enterprise domain
 E.g. \fBgit config machete.github.domain git.example.org\fP
 .TP
-.B Remote name
+.B Remote name (as in \fBgit remote\fP)
 E.g. \fBgit config machete.github.remote origin\fP
 .TP
-.B Organization name
-E.g. \fBgit config machete.github.organization VirtusLab\fP
-.TP
-.B Repository name
-E.g. \fBgit config machete.github.repository git\-machete\fP
+.B Organization and repository name
+E.g. \fBgit config machete.github.organization VirtusLab; git config machete.github.repository git\-machete\fP
 .UNINDENT
 .UNINDENT
 .UNINDENT
 .sp
 Note that you do \fBnot\fP need to set all four keys at once.
 For example, in a typical usage of GitHub Enterprise, it should be enough to just set \fBmachete.github.domain\fP\&.
+Only \fBmachete.github.organization\fP and \fBmachete.github.repository\fP must be specified together.
 .UNINDENT
 .UNINDENT
 .sp
diff --git a/docs/source/cli/config.rst b/docs/source/cli/config.rst
index f6c17f7..dcf6f72 100644
--- a/docs/source/cli/config.rst
+++ b/docs/source/cli/config.rst
@@ -15,7 +15,7 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
         :start-line: 2
 
 ``machete.overrideForkPoint.<branch>.to``:
-    Executing ``git machete fork-point --override-to[-parent|-inferred|=<revision>] [<branch>]`` sets up a fork point override for <branch>.
+    Executing ``git machete fork-point --override-to[-parent|-inferred|=<revision>] [<branch>]`` sets up a fork point override for ``<branch>``.
 
     The override data is stored under ``machete.overrideForkPoint.<branch>.to`` git config key.
 
diff --git a/docs/source/github_config_keys.rst b/docs/source/github_config_keys.rst
index c75b28d..527c0c8 100644
--- a/docs/source/github_config_keys.rst
+++ b/docs/source/github_config_keys.rst
@@ -6,18 +6,16 @@
         GitHub Enterprise domain
             E.g. ``git config machete.github.domain git.example.org``
 
-        Remote name
+        Remote name (as in ``git remote``)
             E.g. ``git config machete.github.remote origin``
 
-        Organization name
-            E.g. ``git config machete.github.organization VirtusLab``
-
-        Repository name
-            E.g. ``git config machete.github.repository git-machete``
+        Organization and repository name
+            E.g. ``git config machete.github.organization VirtusLab; git config machete.github.repository git-machete``
 
     Note that you do **not** need to set all four keys at once.
     For example, in a typical usage of GitHub Enterprise, it should be enough to just set ``machete.github.domain``.
+    Only ``machete.github.organization`` and ``machete.github.repository`` must be specified together.
 
 ..
-    Text order in this file is relevant, if you want to change something, find each ``.. include:: github_config_keys.rst`` instance
-    and if the instance has ``start-line`` or ``end-line`` options provided, make sure that after changes the output text stays the same.
+    Text order in this file is relevant, if you want to change something, find each occurrence of ``.. include:: github_config_keys.rst``
+    and if this occurrence has ``start-line`` or ``end-line`` options provided, make sure that after changes the output text stays the same.
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 4e7422b..4d91972 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -12,8 +12,7 @@ import git_machete.options
 from git_machete import __version__, git_config_keys, utils
 
 from .client import MacheteClient
-from .exceptions import (ExitCode, InteractionStopped, MacheteException,
-                         UnderlyingGitException)
+from .exceptions import ExitCode, MacheteException, UnderlyingGitException
 from .generated_docs import long_docs, short_docs
 from .git_operations import (AnyBranchName, AnyRevision, GitContext,
                              LocalBranchShortName, RemoteBranchShortName)
@@ -865,15 +864,9 @@ def main() -> None:
         sys.exit(ExitCode.END_OF_FILE_SIGNAL)
     except KeyboardInterrupt:
         sys.exit(ExitCode.KEYBOARD_INTERRUPT)
-    except (argparse.ArgumentError, argparse.ArgumentTypeError) as e:
-        print(get_short_general_usage())
-        print(e, file=sys.stderr)
-        sys.exit(ExitCode.ARGUMENT_ERROR)
     except (MacheteException, UnderlyingGitException) as e:
         print(e, file=sys.stderr)
         sys.exit(ExitCode.MACHETE_EXCEPTION)
-    except InteractionStopped:
-        pass
 
 
 if __name__ == "__main__":
diff --git a/git_machete/client.py b/git_machete/client.py
index 794d2f5..9f02a32 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -20,7 +20,8 @@ from .git_operations import (HEAD, AnyBranchName, AnyRevision, BranchPair,
                              GitLogEntry, LocalBranchShortName,
                              RemoteBranchShortName)
 from .github import (GitHubClient, GitHubPullRequest, GitHubToken,
-                     RemoteAndOrganizationAndRepository, is_github_remote_url)
+                     OrganizationAndRepository,
+                     OrganizationAndRepositoryAndRemote, is_github_remote_url)
 from .utils import (GITHUB_NEW_ISSUE_MESSAGE, AnsiEscapeCodes, PopenResult,
                     SyncToParentStatus, bold, colored, debug, dim, excluding,
                     flat_map, fmt, get_pretty_choices, get_second,
@@ -1586,8 +1587,8 @@ class MacheteClient:
 
     def sync_annotations_to_github_prs(self) -> None:
         domain = self.__derive_github_domain()
-        remote_org_repo = self.__derive_remote_and_github_org_and_repo(domain=domain)
-        github_client = GitHubClient(domain=domain, organization=remote_org_repo.organization, repository=remote_org_repo.repository)
+        org_repo_remote = self.__derive_org_repo_and_remote(domain=domain)
+        github_client = GitHubClient(domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
         print('Checking for open GitHub PRs... ', end='', flush=True)
         current_user: Optional[str] = github_client.derive_current_user_login()
         debug('Current GitHub user is ' + (bold(current_user or '<none>')))
@@ -2028,8 +2029,8 @@ class MacheteClient:
                             fail_on_missing_current_user_for_my_opened_prs: bool = False
                             ) -> None:
         domain = self.__derive_github_domain()
-        remote_org_repo = self.__derive_remote_and_github_org_and_repo(domain=domain)
-        github_client = GitHubClient(domain=domain, organization=remote_org_repo.organization, repository=remote_org_repo.repository)
+        org_repo_remote = self.__derive_org_repo_and_remote(domain=domain)
+        github_client = GitHubClient(domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
         print('Checking for open GitHub PRs... ', end='', flush=True)
 
         current_user: Optional[str] = github_client.derive_current_user_login()
@@ -2048,14 +2049,14 @@ class MacheteClient:
             pr_nos, all_opened_prs_from_github=all_open_prs, github_client=github_client,
             all=all_opened_prs, mine=my_opened_prs, by=opened_by, user=current_user)
 
-        debug(f'organization is {remote_org_repo.organization}, repository is {remote_org_repo.repository}')
-        self.__git.fetch_remote(remote_org_repo.remote)
+        debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}')
+        self.__git.fetch_remote(org_repo_remote.remote)
 
         pr: Optional[GitHubPullRequest] = None
         checked_out_prs: List[GitHubPullRequest] = []
         for pr in sorted(applicable_prs, key=lambda x: x.number):
             if pr.full_repository_name:
-                if '/'.join([remote_org_repo.remote, pr.head]) not in self.__git.get_remote_branches():
+                if '/'.join([org_repo_remote.remote, pr.head]) not in self.__git.get_remote_branches():
                     remote_already_added: Optional[str] = self.__get_remote_name_for_repository_url(domain, pr.repository_url)
                     if remote_already_added:
                         remote_to_fetch = remote_already_added
@@ -2063,7 +2064,7 @@ class MacheteClient:
                         remote_to_fetch = pr.full_repository_name.split('/')[0]
                         if remote_to_fetch not in self.__git.get_remotes():
                             self.__git.add_remote(remote_to_fetch, pr.repository_url)
-                    if remote_org_repo.remote != remote_to_fetch:
+                    if org_repo_remote.remote != remote_to_fetch:
                         self.__git.fetch_remote(remote_to_fetch)
                     if '/'.join([remote_to_fetch, pr.head]) not in self.__git.get_remote_branches():
                         raise MacheteException(
@@ -2072,7 +2073,7 @@ class MacheteClient:
             else:
                 warn(f'Pull request #{bold(str(pr.number))} comes from fork and its repository is already deleted. '
                      f'No remote tracking data will be set up for {bold(pr.head)} branch.')
-                github_client.checkout_pr_refs(self.__git, remote_org_repo.remote, pr.number, LocalBranchShortName.of(pr.head))
+                github_client.checkout_pr_refs(self.__git, org_repo_remote.remote, pr.number, LocalBranchShortName.of(pr.head))
             if pr.state == 'closed':
                 warn(f'Pull request #{bold(str(pr.number))} is already closed.')
             debug(f'found {pr}')
@@ -2167,24 +2168,22 @@ class MacheteClient:
 
     def __get_remote_name_for_repository_url(self, github_domain: str, remote_url: str) -> Optional[str]:
         """
-        Check if remote is added locally by its url,
-        because it may happen that a remote is already added under a name different from the name of organization on GitHub
+        Check if the given remote (as identified by `remote_url`) is already added to local git remotes,
+        because it may happen that the local git remote's name is different from the name of organization on GitHub.
         """
         for remote, url in self.__get_url_for_remote().items():
-            url = url if url.endswith('.git') else url + '.git'
-            remote_url = remote_url if remote_url.endswith('.git') else remote_url + '.git'
             if is_github_remote_url(github_domain, url) and \
-                    RemoteAndOrganizationAndRepository.from_url(github_domain, url, remote) == \
-                    RemoteAndOrganizationAndRepository.from_url(github_domain, remote_url, remote):
+                    OrganizationAndRepository.from_url(github_domain, url) == \
+                    OrganizationAndRepository.from_url(github_domain, remote_url):
                 return remote
         return None
 
     def retarget_github_pr(self, head: LocalBranchShortName, ignore_if_missing: bool) -> None:
         domain = self.__derive_github_domain()
-        remote_org_repo = self.__derive_remote_and_github_org_and_repo(domain=domain, branch_used_for_tracking_data=head)
-        github_client = GitHubClient(domain=domain, organization=remote_org_repo.organization, repository=remote_org_repo.repository)
+        org_repo_remote = self.__derive_org_repo_and_remote(domain=domain, branch_used_for_tracking_data=head)
+        github_client = GitHubClient(domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
 
-        debug(f'organization is {remote_org_repo.organization}, repository is {remote_org_repo.repository}')
+        debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}')
 
         try:
             prs: List[GitHubPullRequest] = github_client.derive_pull_requests_by_head(head)
@@ -2223,31 +2222,67 @@ class MacheteClient:
     def __derive_github_domain(self) -> str:
         return self.__git.get_config_attr_or_none(key=git_config_keys.GITHUB_DOMAIN) or GitHubClient.DEFAULT_GITHUB_DOMAIN
 
-    def __derive_remote_and_github_org_and_repo(self,
-                                                domain: str,
-                                                branch_used_for_tracking_data: Optional[LocalBranchShortName] = None
-                                                ) -> RemoteAndOrganizationAndRepository:
-        remote_and_organization_and_repository_from_config = RemoteAndOrganizationAndRepository.from_config(self.__git)
-        if remote_and_organization_and_repository_from_config:
-            return remote_and_organization_and_repository_from_config
+    def __derive_org_repo_and_remote(
+            self,
+            domain: str,
+            branch_used_for_tracking_data: Optional[LocalBranchShortName] = None
+    ) -> OrganizationAndRepositoryAndRemote:
+        remote_from_config = self.__git.get_config_attr_or_none(key=git_config_keys.GITHUB_REMOTE)
+        org_from_config = self.__git.get_config_attr_or_none(key=git_config_keys.GITHUB_ORGANIZATION)
+        repo_from_config = self.__git.get_config_attr_or_none(key=git_config_keys.GITHUB_REPOSITORY)
 
         url_for_remote: Dict[str, str] = self.__get_url_for_remote()
         if not url_for_remote:
-            raise MacheteException(fmt('No remotes defined for this repository (see `git remote`)'))
-
-        remote_and_organization_and_repository_from_urls: Dict[str, RemoteAndOrganizationAndRepository] = {
-            remote: ror for remote, ror in (
-                (remote, RemoteAndOrganizationAndRepository.from_url(domain, url, remote)) for remote, url in url_for_remote.items()
-            ) if ror
+            raise MacheteException('No remotes defined for this repository (see `git remote`)')
+
+        if org_from_config and not repo_from_config:
+            raise MacheteException('`machete.github.organization` git config key is present, '
+                                   'but `machete.github.repository` is missing. Both keys must be present to take effect')
+
+        if not org_from_config and repo_from_config:
+            raise MacheteException('`machete.github.repository` git config key is present, '
+                                   'but `machete.github.organization` is missing. Both keys must be present to take effect')
+
+        if remote_from_config:
+            if remote_from_config not in url_for_remote:
+                raise MacheteException(f'`machete.github.remote` git config key points to `{remote_from_config}` remote, '
+                                       'but such remote does not exist')
+
+        if remote_from_config and org_from_config and repo_from_config:
+            return OrganizationAndRepositoryAndRemote(org_from_config, repo_from_config, remote_from_config)
+
+        if remote_from_config:
+            url = url_for_remote[remote_from_config]
+            org_and_repo = OrganizationAndRepository.from_url(domain, url)
+            if not org_and_repo:
+                raise MacheteException(f'`machete.github.remote` git config key points to `{remote_from_config}` remote, '
+                                       f'but its URL `{url}` does not correspond to a valid GitHub repository')
+            return OrganizationAndRepositoryAndRemote(org_and_repo.organization, org_and_repo.repository, remote_from_config)
+
+        if org_from_config and repo_from_config:
+            for remote, url in self.__get_url_for_remote().items():
+                if is_github_remote_url(domain, url) and \
+                        OrganizationAndRepository.from_url(domain, url) == \
+                        OrganizationAndRepository(org_from_config, repo_from_config):
+                    return OrganizationAndRepositoryAndRemote(org_from_config, repo_from_config, remote)
+            raise MacheteException(
+                'Both `machete.github.organization` and `machete.github.repository` git config keys are defined, '
+                f'but no remote seems to correspond to `{org_from_config}/{repo_from_config}` (organization/repository) on GitHub.\n'
+                'Consider pointing to the remote via `machete.github.remote` config key')
+
+        remote_and_organization_and_repository_from_urls: Dict[str, OrganizationAndRepositoryAndRemote] = {
+            remote: OrganizationAndRepositoryAndRemote(oar.organization, oar.repository, remote) for remote, oar in (
+                (remote, OrganizationAndRepository.from_url(domain, url)) for remote, url in url_for_remote.items()
+            ) if oar
         }
 
         if not remote_and_organization_and_repository_from_urls:
             raise MacheteException(
-                fmt('Remotes are defined for this repository, but none of them '
-                    'seems to correspond to GitHub (see `git remote -v` for details).\n'
-                    'It is possible that you are using a custom GitHub URL.\n'
-                    'If that is the case, you can provide repository information explicitly via some or all of git config keys: '
-                    '`machete.github.{domain,remote,organization,repository}.`\n'))  # noqa: FS003
+                'Remotes are defined for this repository, but none of them '
+                'seems to correspond to GitHub (see `git remote -v` for details).\n'
+                'It is possible that you are using a custom GitHub URL.\n'
+                'If that is the case, you can provide repository information explicitly via some or all of git config keys: '
+                '`machete.github.{domain,remote,organization,repository}.`\n')  # noqa: FS003
 
         if len(remote_and_organization_and_repository_from_urls) == 1:
             return remote_and_organization_and_repository_from_urls[list(remote_and_organization_and_repository_from_urls.keys())[0]]
@@ -2286,15 +2321,15 @@ class MacheteClient:
             raise MacheteException(  # pragma: no cover; should never happen
                 f'Could not determine base branch for PR. Branch {bold(head)} is a root branch.' + GITHUB_NEW_ISSUE_MESSAGE)
         domain = self.__derive_github_domain()
-        remote_org_repo = self.__derive_remote_and_github_org_and_repo(domain=domain, branch_used_for_tracking_data=head)
-        github_client = GitHubClient(domain=domain, organization=remote_org_repo.organization, repository=remote_org_repo.repository)
-        print(f"Fetching {bold(remote_org_repo.remote)}...")
-        self.__git.fetch_remote(remote_org_repo.remote)
-        if '/'.join([remote_org_repo.remote, base]) not in self.__git.get_remote_branches():
+        org_repo_remote = self.__derive_org_repo_and_remote(domain=domain, branch_used_for_tracking_data=head)
+        github_client = GitHubClient(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)
+        if '/'.join([org_repo_remote.remote, base]) not in self.__git.get_remote_branches():
             warn(f'Base branch for this PR ({bold(base)}) is not found on remote, pushing...')
             self.__handle_untracked_branch(
                 branch=base,
-                new_remote=remote_org_repo.remote,
+                new_remote=org_repo_remote.remote,
                 is_called_from_traverse=False,
                 is_called_from_create_pr=True,
                 opt_push_tracked=False,
@@ -2302,7 +2337,7 @@ class MacheteClient:
                 opt_yes=False)
 
         current_user: Optional[str] = github_client.derive_current_user_login()
-        debug(f'organization is {remote_org_repo.organization}, repository is {remote_org_repo.repository}')
+        debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}')
         debug('current GitHub user is ' + (current_user or '<none>'))
 
         description_path = self.__git.get_main_git_subpath('info', 'description')
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index efb16e7..7a9a081 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -155,16 +155,14 @@ long_docs: Dict[str, str] = {
            GitHub Enterprise domain
               E.g. `git config machete.github.domain git.example.org`
 
-           Remote name
+           Remote name (as in `git remote`)
               E.g. `git config machete.github.remote origin`
 
-           Organization name
-              E.g. `git config machete.github.organization VirtusLab`
-
-           Repository name
-              E.g. `git config machete.github.repository git-machete`
+           Organization and repository name
+              E.g. `git config machete.github.organization VirtusLab; git config machete.github.repository git-machete`
         Note that you do <b>not</b> need to set all four keys at once.
         For example, in a typical usage of GitHub Enterprise, it should be enough to just set `machete.github.domain`.
+        Only `machete.github.organization` and `machete.github.repository` must be specified together.
 
         In any other case, sets the annotation for the given/current branch to the given <annotation text>.
         If multiple <annotation text>'s are passed to the command, they are concatenated with a single space.
@@ -223,16 +221,14 @@ long_docs: Dict[str, str] = {
            GitHub Enterprise domain
               E.g. `git config machete.github.domain git.example.org`
 
-           Remote name
+           Remote name (as in `git remote`)
               E.g. `git config machete.github.remote origin`
 
-           Organization name
-              E.g. `git config machete.github.organization VirtusLab`
-
-           Repository name
-              E.g. `git config machete.github.repository git-machete`
+           Organization and repository name
+              E.g. `git config machete.github.organization VirtusLab; git config machete.github.repository git-machete`
         Note that you do <b>not</b> need to set all four keys at once.
         For example, in a typical usage of GitHub Enterprise, it should be enough to just set `machete.github.domain`.
+        Only `machete.github.organization` and `machete.github.repository` must be specified together.
 
         <b>Options:</b>
            <b>-c</b>, <b>--checkout-my-github-prs</b>
@@ -288,21 +284,19 @@ long_docs: Dict[str, str] = {
                  GitHub Enterprise domain
               E.g. `git config machete.github.domain git.example.org`
 
-                 Remote name
+                 Remote name (as in `git remote`)
               E.g. `git config machete.github.remote origin`
 
-                 Organization name
-              E.g. `git config machete.github.organization VirtusLab`
-
-                 Repository name
-              E.g. `git config machete.github.repository git-machete`
+                 Organization and repository name
+              E.g. `git config machete.github.organization VirtusLab; git config machete.github.repository git-machete`
 
               Note that you do <b>not</b> need to set all four keys at once.
               For example, in a typical usage of GitHub Enterprise, it should be enough to just set `machete.github.domain`.
+              Only `machete.github.organization` and `machete.github.repository` must be specified together.
 
            `machete.overrideForkPoint.<branch>.to`:
  
-              Executing `git machete fork-point --override-to[-parent|-inferred|=<revision>] [<branch>]` sets up a fork point override for <branch>.
+              Executing `git machete fork-point --override-to[-parent|-inferred|=<revision>] [<branch>]` sets up a fork point override for `<branch>`.
 
               The override data is stored under `machete.overrideForkPoint.<branch>.to` git config key.
 
@@ -576,16 +570,14 @@ long_docs: Dict[str, str] = {
            GitHub Enterprise domain
               E.g. `git config machete.github.domain git.example.org`
 
-           Remote name
+           Remote name (as in `git remote`)
               E.g. `git config machete.github.remote origin`
 
-           Organization name
-              E.g. `git config machete.github.organization VirtusLab`
-
-           Repository name
-              E.g. `git config machete.github.repository git-machete`
+           Organization and repository name
+              E.g. `git config machete.github.organization VirtusLab; git config machete.github.repository git-machete`
         Note that you do <b>not</b> need to set all four keys at once.
         For example, in a typical usage of GitHub Enterprise, it should be enough to just set `machete.github.domain`.
+        Only `machete.github.organization` and `machete.github.repository` must be specified together.
 
         <b>Subcommands:</b>
            `anno-prs`:
diff --git a/git_machete/github.py b/git_machete/github.py
index a04e3de..2375413 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -9,8 +9,6 @@ import urllib.request
 from pathlib import Path
 from typing import Any, Dict, List, NamedTuple, Optional, Tuple
 
-from git_machete import git_config_keys
-
 from .exceptions import MacheteException, UnprocessableEntityHTTPError
 from .git_operations import GitContext, LocalBranchShortName
 from .utils import bold, debug, fmt, popen_cmd, warn
@@ -43,34 +41,28 @@ class GitHubPullRequest(NamedTuple):
         return f"PR #{self.number} by {self.user}: {self.head} -> {self.base}"
 
 
-class RemoteAndOrganizationAndRepository(NamedTuple):
-    remote: str
+class OrganizationAndRepository(NamedTuple):
     organization: str
     repository: str
 
     @classmethod
-    def from_config(cls, git: GitContext) -> Optional["RemoteAndOrganizationAndRepository"]:
-        remote = git.get_config_attr_or_none(key=git_config_keys.GITHUB_REMOTE)
-        organization = git.get_config_attr_or_none(key=git_config_keys.GITHUB_ORGANIZATION)
-        repository = git.get_config_attr_or_none(key=git_config_keys.GITHUB_REPOSITORY)
-        if remote and organization and repository:
-            return cls(remote, organization, repository)
-        else:
-            return None
-
-    @classmethod
-    def from_url(cls, domain: str, url: str, remote: str) -> Optional["RemoteAndOrganizationAndRepository"]:
+    def from_url(cls, domain: str, url: str) -> Optional["OrganizationAndRepository"]:
+        url = url if url.endswith('.git') else url + '.git'
         for pattern in github_remote_url_patterns(domain):
             match = re.match(pattern, url)
             if match:
                 org = match.group(1)
                 repo = match.group(2)
-                return cls(remote=remote,
-                           organization=org,
-                           repository=repo if repo[-4:] != '.git' else repo[:-4])
+                return cls(organization=org, repository=repo if repo[-4:] != '.git' else repo[:-4])
         return None
 
 
+class OrganizationAndRepositoryAndRemote(NamedTuple):
+    organization: str
+    repository: str
+    remote: str
+
+
 GITHUB_TOKEN_ENV_VAR = 'GITHUB_TOKEN'
 
 
@@ -444,4 +436,5 @@ def github_remote_url_patterns(domain: str) -> List[str]:
 
 
 def is_github_remote_url(domain: str, url: str) -> bool:
+    url = url if url.endswith('.git') else url + '.git'
     return any((re.match(pattern, url) for pattern in github_remote_url_patterns(domain)))
diff --git a/tox.ini b/tox.ini
index 9f64b9d..51f558f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,8 +1,8 @@
 [tox]
 # Environments selected for frequent execution during development
 # Let's still include pep8-check despite a preceding autopep8 run to filter out certain errors
-# (like W293 blank line in multiline strings contains whitespace)
-# that don't seem to ever get fixed by autopep8.
+# (like W293 blank line in multiline strings contains whitespace, or FS003 f-string missing prefix)
+# that don't get fixed by autopep8.
 envlist = pep8,isort,pep8-check,mypy,vulture-check,sphinx-man,py-docs
 minversion = 2.3.2
 skipsdist = True


Test patch:
diff --git a/tests/base_test.py b/tests/base_test.py
index 1be3636..b60c780 100644
--- a/tests/base_test.py
+++ b/tests/base_test.py
@@ -133,7 +133,11 @@ class GitRepositorySandbox:
         return self
 
     def add_remote(self, remote: str, url: str) -> "GitRepositorySandbox":
-        self.execute(f'git remote add {remote} {url}')
+        self.execute(f'git remote add {remote} "{url}"')
+        return self
+
+    def set_remote_url(self, remote: str, url: str) -> "GitRepositorySandbox":
+        self.execute(f'git remote set-url {remote} "{url}"')
         return self
 
     def remove_remote(self, remote: str = 'origin') -> "GitRepositorySandbox":
@@ -164,6 +168,10 @@ class GitRepositorySandbox:
         self.execute(f'git config {key} "{value}"')
         return self
 
+    def unset_git_config_key(self, key: str) -> "GitRepositorySandbox":
+        self.execute(f'git config --unset {key}')
+        return self
+
     def read_file(self, file_name: str) -> str:
         with open(file_name) as f:
             return f.read()
diff --git a/tests/mockers_github.py b/tests/mockers_github.py
index 10ed7ff..8331c64 100644
--- a/tests/mockers_github.py
+++ b/tests/mockers_github.py
@@ -7,9 +7,9 @@ from urllib.error import HTTPError
 from urllib.parse import ParseResult, parse_qs, urlencode, urlparse
 from urllib.request import Request
 
-from git_machete.github import GitHubToken, RemoteAndOrganizationAndRepository
+from git_machete.github import GitHubToken, OrganizationAndRepository
 
-mock_repository_info: Dict[str, str] = {'full_name': 'testing/checkout_prs',
+mock_repository_info: Dict[str, str] = {'full_name': 'tester/repo_sandbox',
                                         'html_url': 'https://github.com/tester/repo_sandbox.git'}
 
 
@@ -21,8 +21,8 @@ def mock_github_token_for_domain_fake(_domain: str) -> GitHubToken:
     return GitHubToken(value='ghp_dummy_token', provider='dummy_provider')
 
 
-def mock_from_url(domain: str, url: str, remote: str) -> "RemoteAndOrganizationAndRepository":  # noqa: U100
-    return RemoteAndOrganizationAndRepository(remote, "example-org", "example-repo")
+def mock_from_url(domain: str, url: str) -> "OrganizationAndRepository":  # noqa: U100
+    return OrganizationAndRepository("example-org", "example-repo")
 
 
 def mock_shutil_which(path: Optional[str]) -> Callable[[Any], Optional[str]]:
diff --git a/tests/test_github.py b/tests/test_github.py
index 9ff57cf..4b2e286 100644
--- a/tests/test_github.py
+++ b/tests/test_github.py
@@ -1,10 +1,12 @@
+from contextlib import contextmanager
 from textwrap import dedent
+from typing import Iterator
 from unittest.mock import mock_open
 
 from pytest_mock import MockerFixture
 
 from git_machete.github import (GitHubClient, GitHubToken,
-                                RemoteAndOrganizationAndRepository)
+                                OrganizationAndRepository)
 from tests.base_test import BaseTest
 from tests.mockers import (assert_failure, assert_success, launch_command,
                            mock__popen_cmd_with_fixed_results,
@@ -29,16 +31,15 @@ class TestGitHub(BaseTest):
         urls = urls + [url + '.git' for url in urls]
 
         for url in urls:
-            remote_and_organization_and_repository = RemoteAndOrganizationAndRepository.from_url(
-                domain=GitHubClient.DEFAULT_GITHUB_DOMAIN, url=url, remote='origin')
-            assert remote_and_organization_and_repository is not None
-            assert remote_and_organization_and_repository.organization == organization
-            assert remote_and_organization_and_repository.repository == repository
+            org_and_repo = OrganizationAndRepository.from_url(domain=GitHubClient.DEFAULT_GITHUB_DOMAIN, url=url)
+            assert org_and_repo is not None
+            assert org_and_repo.organization == organization
+            assert org_and_repo.repository == repository
 
     PR_COUNT_FOR_TEST_GITHUB_API_PAGINATION = 12
 
     github_api_state_for_test_github_api_pagination = MockGitHubAPIState([{
-        'head': {'ref': f'feature_{i:02d}', 'repo': {'full_name': 'testing/checkout_prs',
+        'head': {'ref': f'feature_{i:02d}', 'repo': {'full_name': 'tester/repo_sandbox',
                                                      'html_url': 'https://github.com/tester/repo_sandbox.git'}},
         'user': {'login': 'some_other_user'},
         'base': {'ref': 'develop'},
@@ -51,7 +52,7 @@ class TestGitHub(BaseTest):
         self.patch_symbol(mocker, 'builtins.input', mock_input_returning_y)
         self.patch_symbol(mocker, 'git_machete.github.GitHubClient.MAX_PULLS_PER_PAGE_COUNT', 3)
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none)
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(self.github_api_state_for_test_github_api_pagination))
 
         (
@@ -79,7 +80,7 @@ class TestGitHub(BaseTest):
 
     def test_github_enterprise_domain_unauthorized_without_token(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none)
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitHubAPIState([])))
 
         self.repo_sandbox.set_git_config_key('machete.github.domain', '403.example.org')
@@ -94,7 +95,7 @@ class TestGitHub(BaseTest):
 
     def test_github_enterprise_domain_unauthorized_with_token(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake)
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitHubAPIState([])))
 
         self.repo_sandbox.set_git_config_key('machete.github.domain', '403.example.org')
@@ -121,7 +122,7 @@ class TestGitHub(BaseTest):
 
     def test_github_enterprise_domain(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake)
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(self.github_api_state_for_test_github_enterprise_domain))
 
         github_enterprise_domain = 'git.example.org'
@@ -138,8 +139,47 @@ class TestGitHub(BaseTest):
         )
         launch_command('github', 'checkout-prs', '--all')
 
+    def test_github_invalid_config(self) -> None:
+        @contextmanager
+        def git_config_key(key: str, value: str) -> Iterator[None]:
+            self.repo_sandbox.set_git_config_key(key, value)
+            yield
+            self.repo_sandbox.unset_git_config_key(key)
+
+        with git_config_key('machete.github.organization', "example-org"):
+            assert_failure(
+                ['github', 'checkout-prs', '--all'],
+                "machete.github.organization git config key is present, but machete.github.repository is missing. "
+                "Both keys must be present to take effect")
+
+        with git_config_key('machete.github.repository', "example-repo"):
+            assert_failure(
+                ['github', 'checkout-prs', '--all'],
+                "machete.github.repository git config key is present, but machete.github.organization is missing. "
+                "Both keys must be present to take effect")
+
+        with git_config_key('machete.github.remote', "non-existent-remote"):
+            assert_failure(
+                ['github', 'checkout-prs', '--all'],
+                "machete.github.remote git config key points to non-existent-remote remote, but such remote does not exist")
+
+        with git_config_key('machete.github.organization', "example-org"):
+            with git_config_key('machete.github.repository', "example-repo"):
+                assert_failure(
+                    ['github', 'checkout-prs', '--all'],
+                    'Both machete.github.organization and machete.github.repository git config keys are defined, '
+                    'but no remote seems to correspond to example-org/example-repo (organization/repository) on GitHub.\n'
+                    'Consider pointing to the remote via machete.github.remote config key')
+
+        self.repo_sandbox.add_remote("new-origin", "https://gitlab.com/example-org/example-repo.git")  # not a valid GitHub repo URL
+        with git_config_key('machete.github.remote', "new-origin"):
+            assert_failure(
+                ['github', 'checkout-prs', '--all'],
+                'machete.github.remote git config key points to new-origin remote, '
+                'but its URL https://gitlab.com/example-org/example-repo.git does not correspond to a valid GitHub repository')
+
     def test_github_token_retrieval_order(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'os.path.isfile', lambda _file: False)
         self.patch_symbol(mocker, 'shutil.which', mock_shutil_which(None))
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(self.github_api_state_for_test_github_enterprise_domain))
diff --git a/tests/test_github_anno_prs.py b/tests/test_github_anno_prs.py
index c4a8868..19f484d 100644
--- a/tests/test_github_anno_prs.py
+++ b/tests/test_github_anno_prs.py
@@ -113,7 +113,7 @@ class TestGitHubAnnoPRs(BaseTest):
         (
             self.repo_sandbox
                 .remove_remote('new_origin')
-                .set_git_config_key('machete.github.remote', 'custom_origin')
+                .set_git_config_key('machete.github.remote', 'origin')
                 .set_git_config_key('machete.github.organization', 'custom_user')
                 .set_git_config_key('machete.github.repository', 'custom_repo')
         )
diff --git a/tests/test_github_checkout_prs.py b/tests/test_github_checkout_prs.py
index 6a7a833..16a813e 100644
--- a/tests/test_github_checkout_prs.py
+++ b/tests/test_github_checkout_prs.py
@@ -3,7 +3,7 @@ from tempfile import mkdtemp
 
 from pytest_mock import MockerFixture
 
-from git_machete.github import GitHubClient, RemoteAndOrganizationAndRepository
+from git_machete.github import GitHubClient, OrganizationAndRepository
 from tests.base_test import BaseTest, GitRepositorySandbox
 from tests.mockers import (assert_failure, assert_success, launch_command,
                            rewrite_branch_layout_file)
@@ -81,7 +81,7 @@ class TestGitHubCheckoutPRs(BaseTest):
             },
             {
                 'head': {'ref': 'bugfix/remove-n-option',
-                         'repo': {'full_name': 'testing/checkout_prs', 'html_url': GitRepositorySandbox.second_remote_path}},
+                         'repo': {'full_name': 'tester/repo_sandbox', 'html_url': GitRepositorySandbox.second_remote_path}},
                 'user': {'login': 'some_other_user'},
                 'base': {'ref': 'develop'},
                 'number': '5',
@@ -92,7 +92,7 @@ class TestGitHubCheckoutPRs(BaseTest):
     )
 
     def test_github_checkout_prs(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.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(self.github_api_state_for_test_checkout_prs))
 
@@ -292,12 +292,13 @@ class TestGitHubCheckoutPRs(BaseTest):
             """
         )
 
-        # check against wrong pr number
-        remote_org_repo = RemoteAndOrganizationAndRepository.from_url(domain=GitHubClient.DEFAULT_GITHUB_DOMAIN,
-                                                                      url=self.repo_sandbox.remote_path,
-                                                                      remote='origin')
-        assert remote_org_repo is not None
-        expected_error_message = f"PR #100 is not found in repository {remote_org_repo.organization}/{remote_org_repo.repository}"
+        # check against wrong PR number
+        org_repo = OrganizationAndRepository.from_url(
+            domain=GitHubClient.DEFAULT_GITHUB_DOMAIN,
+            url=self.repo_sandbox.remote_path)
+
+        assert org_repo is not None
+        expected_error_message = f"PR #100 is not found in repository {org_repo.organization}/{org_repo.repository}"
         assert_failure(['github', 'checkout-prs', '100'], expected_error_message)
 
         assert_failure(['github', 'checkout-prs', '19', '100'], expected_error_message)
@@ -320,10 +321,10 @@ class TestGitHubCheckoutPRs(BaseTest):
         )
 
         expected_error_message = "Could not check out PR #5 because its head branch bugfix/remove-n-option " \
-                                 "is already deleted from testing."
+                                 "is already deleted from tester."
         assert_failure(['github', 'checkout-prs', '5'], expected_error_message)
 
-        # Check against pr come from fork
+        # Check against PR coming from fork
         (
             self.repo_sandbox
             .new_branch('bugfix/remove-n-option')
@@ -369,7 +370,7 @@ class TestGitHubCheckoutPRs(BaseTest):
             },
             {
                 'head': {'ref': 'sphinx_export',
-                         'repo': {'full_name': 'testing/checkout_prs', 'html_url': GitRepositorySandbox.second_remote_path}},
+                         'repo': {'full_name': 'tester/repo_sandbox', 'html_url': GitRepositorySandbox.second_remote_path}},
                 'user': {'login': 'some_other_user'},
                 'base': {'ref': 'comments/add_docstrings'},
                 'number': '23',
@@ -380,7 +381,7 @@ class TestGitHubCheckoutPRs(BaseTest):
     )
 
     def test_github_checkout_prs_freshly_cloned(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(self.github_api_state_for_test_github_checkout_prs_fresh_repo))
 
         (
@@ -498,7 +499,7 @@ class TestGitHubCheckoutPRs(BaseTest):
 
     def test_github_checkout_prs_from_fork_with_deleted_repo(self, mocker: MockerFixture) -> None:
         # need to mock fetch_ref due to underlying `git fetch pull/head` calls
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen',
                           mock_urlopen(self.github_api_state_for_test_github_checkout_prs_from_fork_with_deleted_repo))
 
@@ -605,7 +606,7 @@ class TestGitHubCheckoutPRs(BaseTest):
             },
             {
                 'head': {'ref': 'bugfix/remove-n-option',
-                         'repo': {'full_name': 'testing/checkout_prs', 'html_url': GitRepositorySandbox.second_remote_path}},
+                         'repo': {'full_name': 'tester/repo_sandbox', 'html_url': GitRepositorySandbox.second_remote_path}},
                 'user': {'login': 'some_other_user'},
                 'base': {'ref': 'develop'},
                 'number': '5',
@@ -616,7 +617,7 @@ class TestGitHubCheckoutPRs(BaseTest):
     )
 
     def test_github_checkout_prs_of_current_user_and_other_users(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake)
         self.patch_symbol(mocker, 'urllib.request.urlopen',
                           mock_urlopen(self.github_api_state_for_test_github_checkout_prs_of_current_user_and_other_users))
@@ -766,7 +767,7 @@ class TestGitHubCheckoutPRs(BaseTest):
         )
 
     def test_github_checkout_prs_misc_failures_and_warns(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitHubAPIState([])))
 
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none)
@@ -806,12 +807,12 @@ class TestGitHubCheckoutPRs(BaseTest):
             """
         )
 
-    github_api_state_for_test_github_checkout_prs_remote_already_added = MockGitHubAPIState(
+    github_api_state_for_test_github_checkout_prs_single_pr = MockGitHubAPIState(
         [
             {
-                'head': {'ref': 'chore/redundant_checks', 'repo': mock_repository_info},
+                'head': {'ref': 'develop', 'repo': mock_repository_info},
                 'user': {'login': 'some_other_user'},
-                'base': {'ref': 'restrict_access'},
+                'base': {'ref': 'master'},
                 'number': '18',
                 'html_url': 'www.github.com',
                 'state': 'open'
@@ -822,7 +823,7 @@ class TestGitHubCheckoutPRs(BaseTest):
     def test_github_checkout_prs_remote_already_added(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'git_machete.git_operations.GitContext.fetch_remote', lambda _self, _remote: None)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(
-            self.github_api_state_for_test_github_checkout_prs_remote_already_added))
+            self.github_api_state_for_test_github_checkout_prs_single_pr))
         (
             self.repo_sandbox
             .remove_remote("origin")
@@ -830,5 +831,54 @@ class TestGitHubCheckoutPRs(BaseTest):
         )
         assert_failure(
             ["github", "checkout-prs", "--all"],
-            "Could not check out PR #18 because its head branch chore/redundant_checks is already deleted from origin-1."
+            "Could not check out PR #18 because its head branch develop is already deleted from origin-1."
+        )
+
+        (
+            self.repo_sandbox
+            .remove_remote("origin-1")
+            .add_remote("tester", 'https://github.com/tester/lolxd.git')
+        )
+        assert_failure(
+            ["github", "checkout-prs", "--all"],
+            "Could not check out PR #18 because its head branch develop is already deleted from tester."
+        )
+
+    def test_github_checkout_prs_org_and_repo_from_config(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.git_operations.GitContext.fetch_remote', lambda _self, _remote: None)
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(
+            self.github_api_state_for_test_github_checkout_prs_single_pr))
+
+        (
+            self.repo_sandbox
+            .new_branch("master").commit()
+            .new_branch("develop").commit().push()
+        )
+
+        self.repo_sandbox.set_remote_url("origin", "https://github.com/example-org/example-repo.git")
+        self.repo_sandbox.set_git_config_key('machete.github.organization', "example-org")
+        self.repo_sandbox.set_git_config_key('machete.github.repository', "example-repo")
+        assert_success(
+            ['github', 'checkout-prs', '--all'],
+            'Checking for open GitHub PRs... OK\n'
+            'Pull request #18 checked out at local branch develop\n'
+        )
+
+    def test_github_checkout_prs_remote_from_config(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.git_operations.GitContext.fetch_remote', lambda _self, _remote: None)
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(
+            self.github_api_state_for_test_github_checkout_prs_single_pr))
+
+        (
+            self.repo_sandbox
+            .new_branch("master").commit()
+            .new_branch("develop").commit().push()
+        )
+
+        self.repo_sandbox.set_remote_url("origin", "https://github.com/example-org/example-repo.git")
+        self.repo_sandbox.set_git_config_key('machete.github.remote', "origin")
+        assert_success(
+            ['github', 'checkout-prs', '--all'],
+            'Checking for open GitHub PRs... OK\n'
+            'Pull request #18 checked out at local branch develop\n'
         )
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index 8177f20..eae35cb 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -32,7 +32,7 @@ class TestGitHubCreatePR(BaseTest):
     def test_github_create_pr(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'builtins.input', mock_input_returning_y)
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake)
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(self.github_api_state_for_test_create_pr))
 
         (
@@ -116,7 +116,7 @@ class TestGitHubCreatePR(BaseTest):
               x-drop-constraint (untracked)
             """,
         )
-        #  untracked state (can only create pr when branch is pushed)
+        # untracked state (can only create PR when branch is pushed)
         self.repo_sandbox.check_out('chore/fields')
 
         self.repo_sandbox.write_to_file(".git/info/milestone", "42")
@@ -297,7 +297,7 @@ class TestGitHubCreatePR(BaseTest):
 
     def test_github_create_pr_missing_base_branch_on_remote(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'builtins.input', mock_input_returning_y)
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.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(
             self.github_api_state_for_test_create_pr_missing_base_branch_on_remote))
@@ -351,7 +351,7 @@ class TestGitHubCreatePR(BaseTest):
     )
 
     def test_github_create_pr_with_multiple_non_origin_remotes(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.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(self.github_api_state_for_test_github_create_pr_with_multiple_non_origin_remotes))
@@ -412,6 +412,14 @@ class TestGitHubCreatePR(BaseTest):
             "Could not establish remote repository, pull request creation interrupted."
         )
 
+        self.patch_symbol(mocker, 'builtins.input', mock_input_returning('1', 'n'))
+        assert_failure(
+            ['github', 'create-pr'],
+            "Multiple non-origin remotes correspond to GitHub in this repository: origin_1, origin_2 -> aborting.\n"
+            "You can also select the repository by providing some or all of git config keys: "
+            "machete.github.{domain,remote,organization,repository}.\n"  # noqa: FS003
+        )
+
         expected_result = """
         Branch feature is untracked and there's no origin repository.
         [1] origin_1
@@ -591,7 +599,7 @@ class TestGitHubCreatePR(BaseTest):
         )
 
     def test_github_create_pr_for_no_push_qualifier(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.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([])))
 
@@ -627,7 +635,7 @@ class TestGitHubCreatePR(BaseTest):
         )
 
     def test_github_create_pr_for_branch_behind_remote(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.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([])))
 
@@ -691,7 +699,7 @@ class TestGitHubCreatePR(BaseTest):
         )
 
     def test_github_create_pr_for_branch_diverged_from_and_older_than_remote(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.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([])))
 
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index 4b6b6a7..52831e9 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -226,7 +226,7 @@ class TestGitHubRetargetPR(BaseTest):
         launch_command('github', 'retarget-pr', '--branch', 'branch-without-pr', '--ignore-if-missing')
 
     def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(self.github_api_state_for_test_retarget_pr))
 
         branchs_first_commit_msg = "First commit on branch."
@@ -357,7 +357,7 @@ class TestGitHubRetargetPR(BaseTest):
     }])
 
     def test_github_retarget_pr_root_branch(self, mocker: MockerFixture) -> None:
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(self.github_api_state_for_test_retarget_pr_root_branch))
 
         self.repo_sandbox.new_branch("master").commit()
diff --git a/tests/test_github_sync.py b/tests/test_github_sync.py
index bc75d01..d86dac5 100644
--- a/tests/test_github_sync.py
+++ b/tests/test_github_sync.py
@@ -25,7 +25,7 @@ class TestGitHubSync(BaseTest):
 
     def test_github_sync(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'builtins.input', mock_input_returning_y)
-        self.patch_symbol(mocker, 'git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake)
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(self.github_api_state_for_test_github_sync))