Repository: VirtusLab/git-machete
Pull request #814 (2023-02-13 14:28:30)

Release v3.15.1


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 8b42d65..a24463e 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,6 +10,3 @@ updates:
     reviewers:
       - PawelLipski
     target-branch: develop
-    # TODO (#771): remove this pin once sphinx-rtd-theme 1.2.0 is released
-    ignore:
-      - dependency-name: "sphinx"
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index daed8bb..0f74f17 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,9 +1,13 @@
 # Release notes
 
+## New in git-machete 3.15.1
+
+- fixed: in case of red edge, the unique history of a branch never includes commits reachable from its parent
+
 ## New in git-machete 3.15.0
 
 - improved: formatting in `git machete` command prompts and outputs
-- added: `slide-out=no` branch qualifier that control the slide-out behaviour of `git machete traverse`
+- added: `slide-out=no` branch qualifier that controls the slide-out behaviour of `git machete traverse`
 
 ## New in git-machete 3.14.3
 
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 254db7b..f3bb9e9 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.15.0'
+__version__ = '3.15.1'
diff --git a/git_machete/client.py b/git_machete/client.py
index 3a69bab..5a8dc7a 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -7,8 +7,6 @@ import sys
 from collections import OrderedDict
 from typing import Callable, Dict, Generator, List, Optional, Tuple
 
-import git_machete.github
-import git_machete.options
 from git_machete import git_config_keys, utils
 from git_machete.annotation import Annotation
 from git_machete.constants import (DISCOVER_DEFAULT_FRESH_BRANCH_COUNT,
@@ -21,18 +19,9 @@ from git_machete.git_operations import (HEAD, AnyBranchName, AnyRevision,
                                         FullCommitHash, GitContext,
                                         GitLogEntry, LocalBranchShortName,
                                         RemoteBranchShortName)
-from git_machete.github import (DEFAULT_GITHUB_DOMAIN, GitHubPullRequest,
+from git_machete.github import (GitHubClient, GitHubPullRequest, GitHubToken,
                                 RemoteAndOrganizationAndRepository,
-                                add_assignees_to_pull_request,
-                                add_reviewers_to_pull_request,
-                                checkout_pr_refs, create_pull_request,
-                                derive_pull_request_by_head,
-                                derive_pull_requests,
-                                get_github_token_possible_providers,
-                                get_parsed_github_remote_url,
-                                get_pull_request_by_number_or_none,
-                                is_github_remote_url, set_base_of_pull_request,
-                                set_milestone_of_pull_request)
+                                is_github_remote_url)
 from git_machete.utils import (
     AnsiEscapeCodes, SyncToParentStatus, bold, colored, debug, dim, excluding,
     flat_map, fmt, get_pretty_choices, get_second,
@@ -954,10 +943,8 @@ class MacheteClient:
                 try:
                     # We're always using fork point overrides, even when status
                     # is launched from discover().
-                    fork_point_hash_cached[branch_], fork_point_branches_cached[branch_] = self.__fork_point_and_containing_branch_pairs(
-                        branch_,
-                        use_overrides=True,
-                        opt_no_detect_squash_merges=opt_no_detect_squash_merges)
+                    fork_point_hash_cached[branch_], fork_point_branches_cached[branch_] = \
+                        self.__fork_point_and_containing_branch_pairs(branch_, use_overrides=True)
                 except MacheteException:
                     fork_point_hash_cached[branch_], fork_point_branches_cached[branch_] = None, []
             return fork_point_hash_cached[branch_]
@@ -1185,8 +1172,7 @@ class MacheteClient:
 
     def __fork_point_and_containing_branch_pairs(self,
                                                  branch: LocalBranchShortName,
-                                                 use_overrides: bool,
-                                                 opt_no_detect_squash_merges: bool
+                                                 use_overrides: bool
                                                  ) -> Tuple[FullCommitHash, List[BranchPair]]:
         upstream = self.up_branch.get(branch)
 
@@ -1195,7 +1181,7 @@ class MacheteClient:
             if overridden_fp_hash:
                 if upstream and \
                         self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()) and \
-                        not self.__git.is_ancestor_or_equal(upstream.full_name(), overridden_fp_hash.full_name()):
+                        not self.__git.is_ancestor_or_equal(upstream.full_name(), overridden_fp_hash):
                     # We need to handle the case when branch is a descendant of upstream,
                     # but the fork point of branch is overridden to a commit that
                     # is NOT a descendant of upstream. In this case it's more
@@ -1214,11 +1200,17 @@ class MacheteClient:
         except StopIteration:
             if upstream and self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()):
                 debug(
-                    f"cannot find fork point, but {branch} is descendant of its upstream {upstream}; "
+                    f"cannot find fork point, but {branch} is a descendant of its upstream {upstream}; "
                     f"falling back to {upstream} as fork point")
                 return self.__git.get_commit_hash_by_revision(upstream), []
-            else:
-                raise MacheteException(f"Cannot find fork point for branch {bold(branch)}")
+            elif upstream and not self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()):
+                common_ancestor_hash = self.__git.get_merge_base(upstream.full_name(), branch.full_name())
+                if common_ancestor_hash:
+                    debug(
+                        f"cannot find fork point, and {branch} is NOT a descendant of its upstream {upstream}; "
+                        f"falling back to common ancestor of {branch} and {upstream} (commit {common_ancestor_hash}) as fork point")
+                    return common_ancestor_hash, []
+            raise MacheteException(f"Cannot find fork point for branch {bold(branch)}")
         else:
             debug(f"commit {fp_hash} is the most recent point in history of {branch} to occur on "
                   "filtered reflog of any other branch or its remote counterpart "
@@ -1226,18 +1218,31 @@ class MacheteClient:
 
             if upstream and \
                     self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()) and \
-                    not self.__git.is_ancestor_or_equal(upstream.full_name(), fp_hash.full_name()):
+                    not self.__git.is_ancestor_or_equal(upstream.full_name(), fp_hash):
                 # That happens very rarely in practice (typically current head
                 # of any branch, including upstream, should occur on the reflog
                 # of this branch, thus is_ancestor(upstream, branch) should imply
                 # is_ancestor(upstream, FP(branch)), but it's still possible in
                 # case reflog of upstream is incomplete for whatever reason.
                 debug(
-                    f"{upstream} is descendant of its upstream {branch}, but inferred fork point commit {fp_hash} "
-                    f"is NOT a descendant of {upstream}; falling back to {upstream} as fork point")
+                    f"{upstream} is an ancestor of its upstream {branch}, "
+                    f"but the inferred fork point commit {fp_hash} is NOT a descendant of {upstream}; "
+                    f"falling back to {upstream} as fork point")
                 return self.__git.get_commit_hash_by_revision(upstream), []
+            elif upstream and \
+                    not self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()) and \
+                    self.__git.is_ancestor_or_equal(fp_hash, upstream.full_name()):
+
+                common_ancestor_hash = self.__git.get_merge_base(upstream.full_name(), branch.full_name())
+                if common_ancestor_hash:
+                    debug(
+                        f"{upstream} is NOT an ancestor of its upstream {branch}, "
+                        f"but the inferred fork point commit {fp_hash} is an ancestor of {upstream}; "
+                        f"falling back to the common ancestor of {branch} and {upstream} (commit {common_ancestor_hash}) as fork point")
+                    return common_ancestor_hash, []
+                else:
+                    return fp_hash, []
             else:
-                debug(f"choosing commit {fp_hash} as fork point")
                 return fp_hash, containing_branch_pairs
 
     def fork_point(
@@ -1247,8 +1252,7 @@ class MacheteClient:
             *,
             opt_no_detect_squash_merges: bool
     ) -> Optional[FullCommitHash]:
-        hash, containing_branch_pairs = self.__fork_point_and_containing_branch_pairs(
-            branch, use_overrides, opt_no_detect_squash_merges=opt_no_detect_squash_merges)
+        hash, containing_branch_pairs = self.__fork_point_and_containing_branch_pairs(branch, use_overrides)
         return FullCommitHash.of(hash) if hash else None
 
     def diff(self, *, branch: Optional[LocalBranchShortName], opt_stat: bool) -> None:
@@ -1474,11 +1478,12 @@ class MacheteClient:
 
     def sync_annotations_to_github_prs(self) -> None:
         domain = self.__derive_github_domain()
-        _, org, repo = self.__derive_remote_and_github_org_and_repo(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)
         print('Checking for open GitHub PRs... ', end='', flush=True)
-        current_user: Optional[str] = git_machete.github.derive_current_user_login(domain)
+        current_user: Optional[str] = github_client.derive_current_user_login()
         debug('Current GitHub user is ' + (bold(current_user) or '<none>'))
-        all_open_prs: List[GitHubPullRequest] = derive_pull_requests(domain, org, repo)
+        all_open_prs: List[GitHubPullRequest] = github_client.derive_pull_requests()
         print(fmt('<green><b>OK</b></green>'))
         self.__sync_annotations_to_definition_file(all_open_prs, current_user)
 
@@ -1812,10 +1817,8 @@ class MacheteClient:
                         opt_push_untracked=opt_push_untracked,
                         opt_push_tracked=opt_push_tracked,
                         opt_yes=opt_yes)
-                elif ans in ('q', 'quit'):
-                    raise StopInteraction
                 else:
-                    raise MacheteException(f'Cannot create pull request from untracked branch {bold(branch)}')
+                    raise StopInteraction
                 return
 
         relation: int = self.__git.get_relation_to_remote_counterpart(branch, remote_branch)
@@ -1975,45 +1978,41 @@ class MacheteClient:
                             verbose: bool = False,
                             fail_on_missing_current_user_for_my_opened_prs: bool = True
                             ) -> None:
-        org: str
-        repo: str
-        remote: str
         domain = self.__derive_github_domain()
-        remote, org, repo = self.__derive_remote_and_github_org_and_repo(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)
         print('Checking for open GitHub PRs... ', end='', flush=True)
 
-        current_user: Optional[str] = git_machete.github.derive_current_user_login(domain)
+        current_user: Optional[str] = github_client.derive_current_user_login()
         if not current_user and my_opened_prs:
             msg = ("Could not determine current user name, please check that the GitHub API token provided by one of the: "
-                   f"{get_github_token_possible_providers()}is valid.")
+                   f"{GitHubToken.get_possible_providers()}is valid.")
             if fail_on_missing_current_user_for_my_opened_prs:
                 warn(msg)
                 return
             else:
                 raise MacheteException(msg)
-        all_open_prs: List[GitHubPullRequest] = derive_pull_requests(domain, org, repo)
+        all_open_prs: List[GitHubPullRequest] = github_client.derive_pull_requests()
         print(fmt('<green><b>OK</b></green>'))
 
         applicable_prs: List[GitHubPullRequest] = self.__get_applicable_pull_requests(pr_nos,
                                                                                       all_opened_prs_from_github=all_open_prs,
-                                                                                      domain=domain,
-                                                                                      org=org,
-                                                                                      repo=repo,
+                                                                                      github_client=github_client,
                                                                                       all=all_opened_prs,
                                                                                       my=my_opened_prs,
                                                                                       by=opened_by,
                                                                                       user=current_user)
 
-        debug(f'organization is {org}, repository is {repo}')
+        debug(f'organization is {remote_org_repo.organization}, repository is {remote_org_repo.repository}')
         if verbose:
-            print(f"Fetching {bold(remote)}...")
-        self.__git.fetch_remote(remote)
+            print(f"Fetching {bold(remote_org_repo.remote)}...")
+        self.__git.fetch_remote(remote_org_repo.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, pr.head]) not in self.__git.get_remote_branches():
+                if '/'.join([remote_org_repo.remote, pr.head]) not in self.__git.get_remote_branches():
                     remote_already_added: Optional[str] = self.__get_added_remote_name_or_none(domain, pr.repository_url)
                     if not remote_already_added:
                         remote_from_pr: str = pr.full_repository_name.split('/')[0]
@@ -2022,7 +2021,7 @@ class MacheteClient:
                         remote_to_fetch: str = remote_from_pr
                     else:
                         remote_to_fetch = remote_already_added
-                    if remote != remote_to_fetch:
+                    if remote_org_repo.remote != remote_to_fetch:
                         if verbose:
                             print(f"Fetching {bold(remote_to_fetch)}...")
                         self.__git.fetch_remote(remote_to_fetch)
@@ -2035,7 +2034,7 @@ class MacheteClient:
                      f'No remote tracking data will be set up for {bold(pr.head)} branch.')
                 if verbose:
                     print(fmt(f"Checking out {bold(pr.head)} locally..."))
-                checkout_pr_refs(self.__git, remote, pr.number, LocalBranchShortName.of(pr.head))
+                github_client.checkout_pr_refs(self.__git, remote_org_repo.remote, pr.number, LocalBranchShortName.of(pr.head))
                 self.flush_caches()
             if pr.state == 'closed':
                 warn(f'Pull request #{bold(str(pr.number))} is already closed.')
@@ -2083,9 +2082,7 @@ class MacheteClient:
     @staticmethod
     def __get_applicable_pull_requests(prs_list: Optional[List[int]],
                                        all_opened_prs_from_github: List[GitHubPullRequest],
-                                       domain: str,
-                                       org: str,
-                                       repo: str,
+                                       github_client: GitHubClient,
                                        all: bool,
                                        my: bool,
                                        by: Optional[str],
@@ -2098,30 +2095,35 @@ class MacheteClient:
                 if _pr:
                     result.append(_pr)
                 else:
-                    pr_from_github = get_pull_request_by_number_or_none(domain, pr_no, org, repo)
+                    pr_from_github = github_client.get_pull_request_by_number_or_none(pr_no)
                     if pr_from_github:
                         result.append(pr_from_github)
                     else:
-                        raise MacheteException(f"PR #{bold(str(pr_no))} is not found in repository {bold(org)}/{bold(repo)}")
+                        raise MacheteException(f"PR #{bold(str(pr_no))} is not found in repository "
+                                               f"{bold(github_client.organization)}/{bold(github_client.repository)}")
             if not result:
                 raise MacheteException(
-                    f"Given PRs: {', '.join(map(str, prs_list))} are not found in repository {bold(org)}/{bold(repo)}")
+                    f"Given PRs: {', '.join(map(str, prs_list))} are not found in repository "
+                    f"{bold(github_client.organization)}/{bold(github_client.repository)}")
             return result
         if all:
             if not all_opened_prs_from_github:
-                warn(f"Currently there are no pull requests opened in repository {bold(org)}/{bold(repo)}")
+                warn(f"Currently there are no pull requests opened in repository "
+                     f"{bold(github_client.organization)}/{bold(github_client.repository)}")
                 return []
             return all_opened_prs_from_github
         elif my and user:
             result = [pr for pr in all_opened_prs_from_github if pr.user == user]
             if not result:
-                warn(f"Current user {bold(user)} has no open pull request in repository {bold(org)}/{bold(repo)}")
+                warn(f"Current user {bold(user)} has no open pull request in repository "
+                     f"{bold(github_client.organization)}/{bold(github_client.repository)}")
                 return []
             return result
         elif by:
             result = [pr for pr in all_opened_prs_from_github if pr.user == by]
             if not result:
-                warn(f"User {bold(by)} has no open pull request in repository {bold(org)}/{bold(repo)}")
+                warn(f"User {bold(by)} has no open pull request in repository "
+                     f"{bold(github_client.organization)}/{bold(github_client.repository)}")
                 return []
             return result
         return []
@@ -2138,22 +2140,23 @@ class MacheteClient:
         for remote, url in 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 get_parsed_github_remote_url(github_domain, url, remote) == \
-                    get_parsed_github_remote_url(github_domain, remote_url, remote):
+            if is_github_remote_url(github_domain, url) and \
+                    RemoteAndOrganizationAndRepository.from_url(github_domain, url, remote) == \
+                    RemoteAndOrganizationAndRepository.from_url(github_domain, remote_url, remote):
                 return remote
         return None
 
     def retarget_github_pr(self, head: LocalBranchShortName) -> None:
-        org: str
-        repo: str
-        domain: str = self.__derive_github_domain()
-        _, org, repo = self.__derive_remote_and_github_org_and_repo(domain, branch_used_for_tracking_data=head)
+        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)
 
-        debug(f'organization is {org}, repository is {repo}')
+        debug(f'organization is {remote_org_repo.organization}, repository is {remote_org_repo.repository}')
 
-        pr: Optional[GitHubPullRequest] = derive_pull_request_by_head(domain, org, repo, head)
+        pr: Optional[GitHubPullRequest] = github_client.derive_pull_request_by_head(head)
         if not pr:
-            raise MacheteException(f'No PR is opened in {bold(org)}/{bold(repo)} for branch {bold(head)}')
+            raise MacheteException(f'No PR is opened in {bold(remote_org_repo.organization)}/{bold(remote_org_repo.organization)} '
+                                   f'for branch {bold(head)}')
         debug(f'found {pr}')
 
         new_base: Optional[LocalBranchShortName] = self.up_branch.get(LocalBranchShortName.of(head))
@@ -2165,7 +2168,7 @@ class MacheteClient:
                 f' so that {bold(head)} is a child of {bold(pr.base)}.')
 
         if pr.base != new_base:
-            set_base_of_pull_request(domain, org, repo, pr.number, base=new_base)
+            github_client.set_base_of_pull_request(pr.number, base=new_base)
             print(f'The base branch of PR #{bold(str(pr.number))} has been switched to {bold(new_base)}')
         else:
             print(f'The base branch of PR #{bold(str(pr.number))} is already {bold(new_base)}')
@@ -2177,14 +2180,14 @@ class MacheteClient:
         self.save_definition_file()
 
     def __derive_github_domain(self) -> str:
-        return self.__git.get_config_attr_or_none(key=git_config_keys.GITHUB_DOMAIN) or DEFAULT_GITHUB_DOMAIN
+        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: LocalBranchShortName = None
                                                 ) -> RemoteAndOrganizationAndRepository:
-        remote_and_organization_and_repository_from_config = self.__get_remote_and_organization_and_repository_name_from_config()
-        if all(remote_and_organization_and_repository_from_config):
+        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
 
         url_for_remote: Dict[str, str] = {
@@ -2193,12 +2196,12 @@ class MacheteClient:
         if not url_for_remote:
             raise MacheteException(fmt('No remotes defined for this repository (see `git remote`)'))
 
-        remote_and_github_org_and_repo: Dict[str, RemoteAndOrganizationAndRepository] = {
-            remote: get_parsed_github_remote_url(domain, url, remote)
+        remote_and_organization_and_repository_from_urls: Dict[str, RemoteAndOrganizationAndRepository] = {
+            remote: RemoteAndOrganizationAndRepository.from_url(domain, url, remote)
             for remote, url in url_for_remote.items() if is_github_remote_url(domain, url)
         }
 
-        if not remote_and_github_org_and_repo:
+        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'
@@ -2206,30 +2209,25 @@ class MacheteClient:
                     '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_github_org_and_repo) == 1:
-            return remote_and_github_org_and_repo[list(remote_and_github_org_and_repo.keys())[0]]
+        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]]
 
-        if 'origin' in remote_and_github_org_and_repo:
-            return remote_and_github_org_and_repo['origin']
+        if 'origin' in remote_and_organization_and_repository_from_urls:
+            return remote_and_organization_and_repository_from_urls['origin']
 
-        if len(remote_and_github_org_and_repo) > 1 and branch_used_for_tracking_data is not None:
+        if len(remote_and_organization_and_repository_from_urls) > 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()))
+                remotes=list(remote_and_organization_and_repository_from_urls.keys()))
             if remote_for_fetching_of_branch is not None:
-                return remote_and_github_org_and_repo[remote_for_fetching_of_branch]
+                return remote_and_organization_and_repository_from_urls[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_organization_and_repository_from_urls.keys())} -> aborting. \n'
             f'You can also select the repository by providing some or all of git config keys: '
             '`machete.github.{domain,remote,organization,repository}`.\n')  # noqa: FS003
 
-    def __get_remote_and_organization_and_repository_name_from_config(self) -> RemoteAndOrganizationAndRepository:
-        return RemoteAndOrganizationAndRepository(remote=self.__git.get_config_attr_or_none(key=git_config_keys.GITHUB_REMOTE),
-                                                  organization=self.__git.get_config_attr_or_none(key=git_config_keys.GITHUB_ORGANIZATION),
-                                                  repository=self.__git.get_config_attr_or_none(key=git_config_keys.GITHUB_REPOSITORY))
-
     def create_github_pr(
             self,
             *,
@@ -2244,25 +2242,24 @@ class MacheteClient:
         base: Optional[LocalBranchShortName] = self.up_branch.get(LocalBranchShortName.of(head))
         if not base:
             raise MacheteException(f'Could not determine base branch for PR. Branch {bold(head)} is a root branch.')
-        org: str
-        repo: str
         domain = self.__derive_github_domain()
-        remote, org, repo = self.__derive_remote_and_github_org_and_repo(domain, branch_used_for_tracking_data=head)
-        print(f"Fetching {bold(remote)}...")
-        self.__git.fetch_remote(remote)
-        if '/'.join([remote, base]) not in self.__git.get_remote_branches():
+        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():
             warn(f'Base branch for this PR ({bold(base)}) is not found on remote, pushing...')
             self.handle_untracked_branch(
                 branch=base,
-                new_remote=remote,
+                new_remote=remote_org_repo.remote,
                 is_called_from_traverse=False,
                 is_called_from_create_pr=True,
                 opt_push_tracked=False,
                 opt_push_untracked=True,
                 opt_yes=False)
 
-        current_user: Optional[str] = git_machete.github.derive_current_user_login(domain)
-        debug(f'organization is {org}, repository is {repo}')
+        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('current GitHub user is ' + (current_user or '<none>'))
 
         fork_point = self.fork_point(head, use_overrides=True, opt_no_detect_squash_merges=False)
@@ -2274,20 +2271,20 @@ class MacheteClient:
 
         ok_str = '<green><b>OK</b></green>'
         print(f'Creating a {"draft " if opt_draft else ""}PR from {bold(head)} to {bold(base)}... ', end='', flush=True)
-        pr: GitHubPullRequest = create_pull_request(domain, org, repo, head=head, base=base, title=commits[0].subject,
-                                                    description=description, draft=opt_draft)
+        pr: GitHubPullRequest = github_client.create_pull_request(head=head, base=base, title=commits[0].subject,
+                                                                  description=description, draft=opt_draft)
         print(fmt(f'{ok_str}, see `{pr.html_url}`'))
 
         milestone_path: str = self.__git.get_main_git_subpath('info', 'milestone')
         milestone: str = utils.slurp_file_or_empty(milestone_path).strip()
         if milestone:
             print(f'Setting milestone of PR #{bold(str(pr.number))} to {bold(milestone)}... ', end='', flush=True)
-            set_milestone_of_pull_request(domain, org, repo, pr.number, milestone=milestone)
+            github_client.set_milestone_of_pull_request(pr.number, milestone=milestone)
             print(fmt(ok_str))
 
         if current_user:
             print(f'Adding {bold(current_user)} as assignee to PR #{bold(str(pr.number))}... ', end='', flush=True)
-            add_assignees_to_pull_request(domain, org, repo, pr.number, [current_user])
+            github_client.add_assignees_to_pull_request(pr.number, [current_user])
             print(fmt(ok_str))
 
         reviewers_path = self.__git.get_main_git_subpath('info', 'reviewers')
@@ -2297,7 +2294,7 @@ class MacheteClient:
                   f'as reviewer{"s" if len(reviewers) > 1 else ""} to PR #{bold(str(pr.number))}... ',
                   end='', flush=True)
             try:
-                add_reviewers_to_pull_request(domain, org, repo, pr.number, reviewers)
+                github_client.add_reviewers_to_pull_request(pr.number, reviewers)
             except UnprocessableEntityHTTPError as e:
                 if 'Reviews may only be requested from collaborators.' in e.msg:
                     warn(f"There are some invalid reviewers in {self.__git.get_main_git_subpath('info', 'reviewers')} file.\n"
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index d7415fd..078a719 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -749,7 +749,12 @@ class GitContext:
             raise MacheteException("Not currently on any branch")
         return result
 
-    def __get_merge_base(self, hash1: FullCommitHash, hash2: FullCommitHash) -> FullCommitHash:
+    def __get_merge_base_for_commit_hashes(self, hash1: FullCommitHash, hash2: FullCommitHash) -> FullCommitHash:
+        # This if statement is not changing the outcome of the later return, but
+        # it enhances the efficiency of the script. If both hashes are the same,
+        # there is no point running git merge-base.
+        if hash1 == hash2:
+            return hash1
         if hash1 > hash2:
             hash1, hash2 = hash2, hash1
         if not (hash1, hash2) in self.__merge_base_cached:
@@ -776,13 +781,16 @@ class GitContext:
     ) -> bool:
         earlier_hash = self.get_commit_hash_by_revision(earlier_revision)
         later_hash = self.get_commit_hash_by_revision(later_revision)
-        # This if statement is not changing the outcome of the later return, but
-        # it enhances the efficiency of the script. If both hashes are the same,
-        # there is no point running git merge-base.
-        if earlier_hash == later_hash:
-            return True
+        return self.__get_merge_base_for_commit_hashes(earlier_hash, later_hash) == earlier_hash
 
-        return self.__get_merge_base(earlier_hash, later_hash) == earlier_hash
+    def get_merge_base(
+            self,
+            earlier_revision: AnyRevision,
+            later_revision: AnyRevision,
+    ) -> Optional[FullCommitHash]:
+        earlier_hash = self.get_commit_hash_by_revision(earlier_revision)
+        later_hash = self.get_commit_hash_by_revision(later_revision)
+        return self.__get_merge_base_for_commit_hashes(earlier_hash, later_hash)
 
     # Determine if reachable_from, or any ancestors of reachable_from that are NOT ancestors of equivalent_to,
     # contain a tree with identical contents to equivalent_to, indicating that
diff --git a/git_machete/github.py b/git_machete/github.py
index e3c103c..2355c40 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -8,27 +8,14 @@ import urllib.error
 # Deliberately NOT using much more convenient `requests` to avoid external dependencies in production code
 import urllib.request
 from pathlib import Path
-from typing import Any, Dict, List, NamedTuple, Optional
+from typing import Any, Dict, List, Optional
 
+from git_machete import git_config_keys
 from git_machete.exceptions import (MacheteException,
                                     UnprocessableEntityHTTPError)
 from git_machete.git_operations import GitContext, LocalBranchShortName
 from git_machete.utils import bold, debug, fmt, warn
 
-GITHUB_TOKEN_ENV_VAR = 'GITHUB_TOKEN'
-DEFAULT_GITHUB_DOMAIN = "github.com"
-
-
-def github_remote_url_patterns(domain: str) -> List[str]:
-    # GitHub DOES NOT allow trailing `.git` suffix in the repository name (also applies to multiple repetitions e.g. `repo_name.git.git`)
-    domain_regex = re.escape(domain)
-    return [
-        f"^https://.*@{domain_regex}/(.*)/(.*)$",
-        f"^https://{domain_regex}/(.*)/(.*)$",
-        f"^git@{domain_regex}:(.*)/(.*)$",
-        f"^ssh://git@{domain_regex}/(.*)/(.*)$"
-    ]
-
 
 class GitHubPullRequest(object):
     def __init__(self,
@@ -41,57 +28,129 @@ class GitHubPullRequest(object):
                  full_repository_name: str,
                  repository_url: str
                  ) -> None:
-        self.number: int = number
-        self.user: str = user
-        self.base: str = base
-        self.head: str = head
-        self.html_url: str = html_url
-        self.state: str = state
-        self.full_repository_name: str = full_repository_name
-        self.repository_url: str = repository_url
+        self.number = number
+        self.user = user
+        self.base = base
+        self.head = head
+        self.html_url = html_url
+        self.state = state
+        self.full_repository_name = full_repository_name
+        self.repository_url = repository_url
+
+    @classmethod
+    def from_json(cls,
+                  pr_json: Dict[str, Any]
+                  ) -> "GitHubPullRequest":
+        return cls(number=int(pr_json['number']),
+                   user=pr_json['user']['login'],
+                   base=pr_json['base']['ref'],
+                   head=pr_json['head']['ref'],
+                   html_url=pr_json['html_url'],
+                   state=pr_json['state'],
+                   full_repository_name=pr_json['head']['repo']['full_name'] if pr_json['head']['repo'] else None,
+                   repository_url=pr_json['head']['repo']['html_url'] if pr_json['head']['repo'] else None)
 
     def __repr__(self) -> str:
         return f"PR #{self.number} by {self.user}: {self.head} -> {self.base}"
 
 
-class RemoteAndOrganizationAndRepository(NamedTuple):
-    remote: str
-    organization: str
-    repository: str
-
-
-def __parse_pr_json(pr_json: Any) -> GitHubPullRequest:
-    return GitHubPullRequest(number=int(pr_json['number']),
-                             user=pr_json['user']['login'],
-                             base=pr_json['base']['ref'],
-                             head=pr_json['head']['ref'],
-                             html_url=pr_json['html_url'],
-                             state=pr_json['state'],
-                             full_repository_name=pr_json['head']['repo']['full_name'] if pr_json['head']['repo'] else None,
-                             repository_url=pr_json['head']['repo']['html_url'] if pr_json['head']['repo'] else None)
-
-
-class GithubTokenAndTokenProvider(NamedTuple):
-    token: str
-    token_provider: str
+class RemoteAndOrganizationAndRepository:
+    def __init__(self,
+                 remote: Optional[str],
+                 organization: Optional[str],
+                 repository: Optional[str]
+                 ) -> None:
+        self.remote = remote
+        self.organization = organization
+        self.repository = repository
+
+    def __bool__(self) -> bool:
+        return self.remote is not None and self.organization is not None and self.repository is not None
+
+    @classmethod
+    def from_config(cls,
+                    git: GitContext
+                    ) -> "RemoteAndOrganizationAndRepository":
+        return 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))
+
+    @classmethod
+    def from_url(cls,
+                 domain: str,
+                 url: str,
+                 remote: str
+                 ) -> Optional["RemoteAndOrganizationAndRepository"]:
+        for pattern in github_remote_url_patterns(domain):
+            match = re.match(pattern, url)
+            if match:
+                org = match.group(1)
+                repo = match.group(2)
+                return RemoteAndOrganizationAndRepository(remote=remote,
+                                                          organization=org,
+                                                          repository=repo if repo[-4:] != '.git' else repo[:-4])
+        return RemoteAndOrganizationAndRepository(remote=None,
+                                                  organization=None,
+                                                  repository=None)
+
+
+class GitHubToken:
+    GITHUB_TOKEN_ENV_VAR = 'GITHUB_TOKEN'
 
+    def __init__(self,
+                 domain: str
+                 ) -> None:
+        self.__domain = domain
+        self.__value: Optional[str] = None
+        self.__provider: Optional[str] = None
+        for token_retrieval_method in [self.__get_token_from_hub, self.__get_token_from_gh, self.__get_token_from_env,
+                                       self.__get_token_from_file_in_home_directory]:
+            if not (self.value and self.provider):
+                token_retrieval_method()
+
+    def __bool__(self) -> bool:
+        return self.__value is not None and self.__provider is not None
+
+    def __get_token_from_hub(self) -> None:
+        home_path: str = str(Path.home())
+        config_hub_path: str = os.path.join(home_path, ".config", "hub")
+        if os.path.isfile(config_hub_path):
+            with open(config_hub_path) as config_hub:
+                # ~/.config/hub is a yaml file, with a structure similar to:
+                #
+                # {domain1}:
+                # - user: {username1}
+                #   oauth_token: *******************
+                #   protocol: {protocol}
+                #
+                # {domain2}:
+                # - user: {username2}
+                #   oauth_token: *******************
+                #   protocol: {protocol}
+                found_host = False
+                for line in config_hub.readlines():
+                    if line.rstrip() == self.__domain + ":":
+                        found_host = True
+                    elif found_host and line.lstrip().startswith("oauth_token:"):
+                        result = re.sub(' *oauth_token: *', '', line).rstrip().replace('"', '')
+                        self.__value = result
+                        self.__provider = f'auth token for {self.__domain} from `hub` GitHub CLI'
 
-def __get_github_token_and_provider(domain: str) -> Optional[GithubTokenAndTokenProvider]:
-    def get_token_from_gh() -> Optional[GithubTokenAndTokenProvider]:
+    def __get_token_from_gh(self) -> None:
         # Abort without error if `gh` isn't available
         gh = shutil.which('gh')
         if not gh:
-            return None
+            return
 
         # There is also `gh auth token`, but it's only been added in gh v2.17.0, in Oct 2022.
         # Let's stick to the older `gh auth status --show-token` for compatibility.
         proc = subprocess.run(
-            [gh, "auth", "status", "--hostname", domain, "--show-token"],
+            [gh, "auth", "status", "--hostname", self.__domain, "--show-token"],
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE
         )
         if proc.returncode != 0:
-            return None
+            return
 
         # `gh auth status --show-token` outputs to stderr in the form:
         #
@@ -104,282 +163,296 @@ def __get_github_token_and_provider(domain: str) -> Optional[GithubTokenAndToken
         stderr = proc.stderr.decode()
         match = re.search(r"Token: (\w+)", stderr)
         if match:
-            return GithubTokenAndTokenProvider(token=match.group(1),
-                                               token_provider=f'auth token for {domain} from `hub` GitHub CLI')
-        return None
+            self.__value = match.group(1)
+            self.__provider = f'auth token for {self.__domain} from `hub` GitHub CLI'
 
-    def get_token_from_hub() -> Optional[GithubTokenAndTokenProvider]:
-        home_path: str = str(Path.home())
-        config_hub_path: str = os.path.join(home_path, ".config", "hub")
-        if os.path.isfile(config_hub_path):
-            with open(config_hub_path) as config_hub:
-                # ~/.config/hub is a yaml file, with a structure similar to:
-                #
-                # {domain1}:
-                # - user: {username1}
-                #   oauth_token: *******************
-                #   protocol: {protocol}
-                #
-                # {domain2}:
-                # - user: {username2}
-                #   oauth_token: *******************
-                #   protocol: {protocol}
-                found_host = False
-                for line in config_hub.readlines():
-                    if line.rstrip() == domain + ":":
-                        found_host = True
-                    elif found_host and line.lstrip().startswith("oauth_token:"):
-                        result = re.sub(' *oauth_token: *', '', line).rstrip().replace('"', '')
-                        return GithubTokenAndTokenProvider(token=result,
-                                                           token_provider=f'auth token for {domain} from `hub` GitHub CLI')
-        return None
-
-    def get_token_from_env() -> Optional[GithubTokenAndTokenProvider]:
-        github_token = os.environ.get(GITHUB_TOKEN_ENV_VAR)
+    def __get_token_from_env(self) -> None:
+        github_token = os.environ.get(self.GITHUB_TOKEN_ENV_VAR)
         if github_token:
-            return GithubTokenAndTokenProvider(token=github_token,
-                                               token_provider=f'`{GITHUB_TOKEN_ENV_VAR}` environment variable')
-        return None
+            self.__value = github_token
+            self.__provider = f'`{self.GITHUB_TOKEN_ENV_VAR}` environment variable'
 
-    def get_token_from_file_in_home_directory() -> Optional[GithubTokenAndTokenProvider]:
+    def __get_token_from_file_in_home_directory(self) -> None:
         required_file_name = '.github-token'
-        token_provider = f'auth token for {domain} from `~/.github-token`'
+        self.__provider = f'auth token for {self.__domain} from `~/.github-token`'
         file_full_path = os.path.expanduser(f'~/{required_file_name}')
 
         if os.path.isfile(file_full_path):
             with open(file_full_path) as file:
                 for line in file.readlines():
-                    if line.endswith(" " + domain):
+                    if line.endswith(" " + self.__domain):
                         token = line.split(" ")[0]
-                        return GithubTokenAndTokenProvider(token=token, token_provider=token_provider)
-                    elif domain == DEFAULT_GITHUB_DOMAIN and " " not in line.rstrip():
-                        return GithubTokenAndTokenProvider(token=line.rstrip(), token_provider=token_provider)
-        return None
-
-    return (get_token_from_env() or
-            get_token_from_file_in_home_directory() or
-            get_token_from_gh() or
-            get_token_from_hub())
-
-
-def __get_github_token(domain: str) -> Optional[str]:
-    github_token_and_provider = __get_github_token_and_provider(domain)
-    return github_token_and_provider.token if github_token_and_provider else None
-
-
-def __get_github_token_provider(domain: str) -> Optional[str]:
-    github_token_and_provider = __get_github_token_and_provider(domain)
-    return github_token_and_provider.token_provider if github_token_and_provider else None
-
-
-def __extract_failure_info_from_422(response: Any) -> str:
-    if response['message'] != 'Validation Failed':
-        return str(response['message'])
-    ret: List[str] = []
-    if response.get('errors'):
-        for error in response['errors']:
-            if error.get('message'):
-                ret.append(error['message'])
-    if ret:
-        return '\n'.join(ret)
-    return str(response)
-
-
-def __fire_github_api_request(domain: str, method: str, path: str,
-                              token: Optional[str], request_body: Optional[Dict[str, Any]] = None) -> Any:
-    headers: Dict[str, str] = {
-        'Content-type': 'application/json',
-        'User-Agent': 'git-machete',
-        'Accept': 'application/vnd.github.v3+json'
-    }
-    if token:
-        headers['Authorization'] = 'Bearer ' + token
-
-    if domain == DEFAULT_GITHUB_DOMAIN:
-        url_prefix = 'https://api.' + domain
-    else:
-        url_prefix = 'https://' + domain + '/api/v3'
-
-    url = url_prefix + path
-    json_body: Optional[str] = json.dumps(request_body) if request_body else None
-    http_request = urllib.request.Request(url, headers=headers, data=json_body.encode() if json_body else None, method=method.upper())
-    debug(f'firing a {method} request to {url} with {"a" if token else "no"} bearer token and request body {json_body or "<none>"}')
-
-    try:
-        with urllib.request.urlopen(http_request) as response:
-            parsed_response_body: Any = json.loads(response.read().decode())
-            # https://docs.github.com/en/rest/guides/using-pagination-in-the-rest-api?apiVersion=2022-11-28#using-link-headers
-            link_header: str = response.info()["link"]
-            if link_header:
-                url_prefix_regex = re.escape(url_prefix)
-                match = re.search(f'<{url_prefix_regex}(/[^>]+)>; rel="next"', link_header)
-                if match:
-                    next_page_path = match.group(1)
-                    debug(f'there is more data to retrieve under {next_page_path}')
-                    return parsed_response_body + __fire_github_api_request(domain, method, next_page_path, token, request_body)
-            return parsed_response_body
-    except urllib.error.HTTPError as err:
-        if err.code == http.HTTPStatus.UNPROCESSABLE_ENTITY:
-            error_response = json.loads(err.read().decode())
-            error_reason: str = __extract_failure_info_from_422(error_response)
-            raise UnprocessableEntityHTTPError(error_reason)
-        elif err.code in (http.HTTPStatus.UNAUTHORIZED, http.HTTPStatus.FORBIDDEN):
-            first_line = f'GitHub API returned `{err.code}` HTTP status with error message: `{err.reason}`\n'
-            if token:
-                raise MacheteException(first_line + 'Make sure that the GitHub API token '
-                                                    f'provided by the {__get_github_token_provider(domain)} '
-                                                    f'is valid and allows for access to `{method.upper()}` `{url_prefix}{path}`.\n'
-                                                    'You can also use a different token provider, available providers can be found '
-                                                    'when running `git machete help github`.')
-            else:
+                        self.__value = token
+                    elif self.__domain == GitHubClient.DEFAULT_GITHUB_DOMAIN and " " not in line.rstrip():
+                        self.__value = line.rstrip()
+
+    @property
+    def value(self) -> Optional[str]:
+        return self.__value
+
+    @property
+    def provider(self) -> Optional[str]:
+        return self.__provider
+
+    @classmethod
+    def get_possible_providers(cls) -> str:
+        return (f'\n\t1. `{cls.GITHUB_TOKEN_ENV_VAR}` environment variable.\n'
+                '\t2. Content of the `~/.github-token` file.\n'
+                '\t3. Current auth token from the `gh` GitHub CLI.\n'
+                '\t4. Current auth token from the `hub` GitHub CLI.\n')
+
+
+class GitHubClient:
+    DEFAULT_GITHUB_DOMAIN = "github.com"
+
+    def __init__(self,
+                 domain: str,
+                 organization: str,
+                 repository: str
+                 ) -> None:
+        self.__domain: str = domain
+        self.__organization: str = organization
+        self.__repository: str = repository
+        self.__token: GitHubToken = GitHubToken(domain=domain)
+
+    @property
+    def organization(self) -> str:
+        return self.__organization
+
+    @property
+    def repository(self) -> str:
+        return self.__repository
+
+    def __fire_github_api_request(self,
+                                  method: str,
+                                  path: str,
+                                  request_body: Optional[Dict[str, Any]] = None
+                                  ) -> Any:
+        headers: Dict[str, str] = {
+            'Content-type': 'application/json',
+            'User-Agent': 'git-machete',
+            'Accept': 'application/vnd.github.v3+json'
+        }
+        if self.__token:
+            headers['Authorization'] = 'Bearer ' + self.__token.value
+
+        if self.__domain == self.DEFAULT_GITHUB_DOMAIN:
+            url_prefix = 'https://api.' + self.__domain
+        else:
+            url_prefix = 'https://' + self.__domain + '/api/v3'
+
+        url = url_prefix + path
+        json_body: Optional[str] = json.dumps(request_body) if request_body else None
+        http_request = urllib.request.Request(url, headers=headers, data=json_body.encode() if json_body else None, method=method.upper())
+        debug(f'firing a {method} request to {url} with {"a" if self.__token else "no"} '
+              f'bearer token and request body {json_body or "<none>"}')
+
+        try:
+            with urllib.request.urlopen(http_request) as response:
+                parsed_response_body: Any = json.loads(response.read().decode())
+                # https://docs.github.com/en/rest/guides/using-pagination-in-the-rest-api?apiVersion=2022-11-28#using-link-headers
+                link_header: str = response.info()["link"]
+                if link_header:
+                    url_prefix_regex = re.escape(url_prefix)
+                    match = re.search(f'<{url_prefix_regex}(/[^>]+)>; rel="next"', link_header)
+                    if match:
+                        next_page_path = match.group(1)
+                        debug(f'there is more data to retrieve under {next_page_path}')
+                        return parsed_response_body + self.__fire_github_api_request(method, next_page_path, request_body)
+                return parsed_response_body
+        except urllib.error.HTTPError as err:
+            if err.code == http.HTTPStatus.UNPROCESSABLE_ENTITY:
+                error_response = json.loads(err.read().decode())
+                error_reason: str = self.__extract_failure_info_from_422(error_response)
+                raise UnprocessableEntityHTTPError(error_reason)
+            elif err.code in (http.HTTPStatus.UNAUTHORIZED, http.HTTPStatus.FORBIDDEN):
+                first_line = f'GitHub API returned `{err.code}` HTTP status with error message: `{err.reason}`\n'
+                if self.__token:
+                    raise MacheteException(first_line + 'Make sure that the GitHub API token '
+                                                        f'provided by the {self.__token.provider} '
+                                                        f'is valid and allows for access to `{method.upper()}` `{url_prefix}{path}`.\n'
+                                                        'You can also use a different token provider, available providers can be found '
+                                                        'when running `git machete help github`.')
+                else:
+                    raise MacheteException(
+                        first_line + f'You might not have the required permissions for this repository.\n'
+                                     f'Provide a GitHub API token with `repo` access via {self.__token.provider}.\n'
+                                     f'Visit `https://{self.__domain}/settings/tokens` to generate a new one.\n'
+                                     'You can also use a different token provider, available providers can be found '
+                                     'when running `git machete help github`.')
+            elif err.code == http.HTTPStatus.NOT_FOUND:
                 raise MacheteException(
-                    first_line + f'You might not have the required permissions for this repository.\n'
-                                 f'Provide a GitHub API token with `repo` access via {__get_github_token_provider(domain)}.\n'
-                                 f'Visit `https://{domain}/settings/tokens` to generate a new one.\n'
-                                 'You can also use a different token provider, available providers can be found '
-                                 'when running `git machete help github`.')
-        elif err.code == http.HTTPStatus.NOT_FOUND:
-            raise MacheteException(
-                f'`{method} {url}` request ended up in 404 response from GitHub. A valid GitHub API token is required.\n'
-                f'Provide a GitHub API token with `repo` access via one of the: {get_github_token_possible_providers()} '
-                f'Visit `https://{domain}/settings/tokens` to generate a new one.')  # TODO (#164): make dedicated exception here
-        elif err.code == http.HTTPStatus.TEMPORARY_REDIRECT:
-            if err.headers['Location'] is not None:
-                if len(err.headers['Location'].split('/')) >= 5:
-                    current_repo_and_org = get_repo_and_org_names_by_id(domain, err.headers['Location'].split('/')[4])
+                    f'`{method} {url}` request ended up in 404 response from GitHub. A valid GitHub API token is required.\n'
+                    f'Provide a GitHub API token with `repo` access via one of the: {GitHubToken.get_possible_providers()} '
+                    f'Visit `https://{self.__domain}/settings/tokens` to generate a new one.')  # TODO (#164): make dedicated exception here
+            elif err.code == http.HTTPStatus.TEMPORARY_REDIRECT:
+                if err.headers['Location'] is not None:
+                    if len(err.headers['Location'].split('/')) >= 5:
+                        current_repo_and_org = self.get_repo_and_org_names_by_id(err.headers['Location'].split('/')[4])
+                else:
+                    first_line = fmt(f'GitHub API returned `{err.code}` HTTP status with error message: `{err.reason}`\n')
+                    raise MacheteException(
+                        first_line + 'It looks like the organization or repository name got changed recently and is outdated.\n'
+                        'Inferring current organization or repository... -> Cannot infer current organization or repository\n'
+                        'Update your remote repository manually via: `git remote set-url <remote_name> <new_repository_url>`.')
+                # err.headers is a case-insensitive dict of class Message with the `__getitem__` and `get` functions implemented in
+                # https://github.com/python/cpython/blob/3.10/Lib/email/message.py
+                pulls_or_issues_api_suffix = "/".join(path.split("/")[4:])
+                new_path = f'/repos/{current_repo_and_org}/{pulls_or_issues_api_suffix}'
+                # for example when creating a new PR, new_path='/repos/new_org_name/new_repo_name/pulls'
+                warn(f'GitHub API returned `{err.code}` HTTP status with error message: `{err.reason}`. \n'
+                     'It looks like the organization or repository name got changed recently and is outdated.\n'
+                     'Inferring current organization or repository... '
+                     f'New organization = {bold(current_repo_and_org.split("/")[0])}, '
+                     f'new repository = {bold(current_repo_and_org.split("/")[1])}.\n'
+                     'You can update your remote repository via: `git remote set-url <remote_name> <new_repository_url>`.',
+                     end='')
+                return self.__fire_github_api_request(method=method, path=new_path, request_body=request_body)
             else:
                 first_line = fmt(f'GitHub API returned `{err.code}` HTTP status with error message: `{err.reason}`\n')
-                raise MacheteException(
-                    first_line + 'It looks like the organization or repository name got changed recently and is outdated.\n'
-                    'Inferring current organization or repository... -> Cannot infer current organization or repository\n'
-                    'Update your remote repository manually via: `git remote set-url <remote_name> <new_repository_url>`.')
-            # err.headers is a case-insensitive dict of class Message with the `__getitem__` and `get` functions implemented in
-            # https://github.com/python/cpython/blob/3.10/Lib/email/message.py
-            pulls_or_issues_api_suffix = "/".join(path.split("/")[4:])
-            new_path = f'/repos/{current_repo_and_org}/{pulls_or_issues_api_suffix}'
-            # for example when creating a new PR, new_path='/repos/new_org_name/new_repo_name/pulls'
-            warn(f'GitHub API returned `{err.code}` HTTP status with error message: `{err.reason}`. \n'
-                 'It looks like the organization or repository name got changed recently and is outdated.\n'
-                 'Inferring current organization or repository... '
-                 f'New organization = {bold(current_repo_and_org.split("/")[0])}, '
-                 f'new repository = {bold(current_repo_and_org.split("/")[1])}.\n'
-                 'You can update your remote repository via: `git remote set-url <remote_name> <new_repository_url>`.',
-                 end='')
-            return __fire_github_api_request(domain=domain, method=method, path=new_path, token=token, request_body=request_body)
+                raise MacheteException(first_line + "Please open an issue regarding this topic under link: "
+                                                    "https://github.com/VirtusLab/git-machete/issues/new")
+        except OSError as e:
+            raise MacheteException(f'Could not connect to {url_prefix}: {e}')
+
+    @staticmethod
+    def __extract_failure_info_from_422(response: Any) -> str:
+        if response['message'] != 'Validation Failed':
+            return str(response['message'])
+        ret: List[str] = []
+        if response.get('errors'):
+            for error in response['errors']:
+                if error.get('message'):
+                    ret.append(error['message'])
+        if ret:
+            return '\n'.join(ret)
+        return str(response)
+
+    def create_pull_request(self,
+                            head: str,
+                            base: str,
+                            title: str,
+                            description: str,
+                            draft: bool
+                            ) -> GitHubPullRequest:
+        request_body: Dict[str, Any] = {
+            'head': head,
+            'base': base,
+            'title': title,
+            'body': description,
+            'draft': draft
+        }
+        pr = self.__fire_github_api_request(method='POST',
+                                            path=f'/repos/{self.__organization}/{self.__repository}/pulls',
+                                            request_body=request_body)
+        return GitHubPullRequest.from_json(pr)
+
+    def add_assignees_to_pull_request(self,
+                                      number: int,
+                                      assignees: List[str]
+                                      ) -> None:
+        request_body: Dict[str, List[str]] = {
+            'assignees': assignees
+        }
+        # Adding assignees is only available via the Issues API, not PRs API.
+        self.__fire_github_api_request(method='POST',
+                                       path=f'/repos/{self.__organization}/{self.__repository}/issues/{number}/assignees',
+                                       request_body=request_body)
+
+    def add_reviewers_to_pull_request(self,
+                                      number: int,
+                                      reviewers: List[str]
+                                      ) -> None:
+        request_body: Dict[str, List[str]] = {
+            'reviewers': reviewers
+        }
+        self.__fire_github_api_request(method='POST',
+                                       path=f'/repos/{self.__organization}/{self.__repository}/pulls/{number}/requested_reviewers',
+                                       request_body=request_body)
+
+    def set_base_of_pull_request(self,
+                                 number: int,
+                                 base: LocalBranchShortName
+                                 ) -> None:
+        request_body: Dict[str, str] = {'base': base}
+        self.__fire_github_api_request(method='PATCH',
+                                       path=f'/repos/{self.__organization}/{self.__repository}/pulls/{number}',
+                                       request_body=request_body)
+
+    def set_milestone_of_pull_request(self,
+                                      number: int,
+                                      milestone: str
+                                      ) -> None:
+        request_body: Dict[str, str] = {'milestone': milestone}
+        # Setting milestone is only available via the Issues API, not PRs API.
+        self.__fire_github_api_request(method='PATCH',
+                                       path=f'/repos/{self.__organization}/{self.__repository}/issues/{number}',
+                                       request_body=request_body)
+
+    def derive_pull_request_by_head(self,
+                                    head: LocalBranchShortName
+                                    ) -> Optional[GitHubPullRequest]:
+        path = f'/repos/{self.__organization}/{self.__repository}/pulls?head={self.__organization}:{head}'
+        prs = self.__fire_github_api_request(method='GET',
+                                             path=path)
+        if len(prs) >= 1:
+            return GitHubPullRequest.from_json(prs[0])
         else:
-            first_line = fmt(f'GitHub API returned `{err.code}` HTTP status with error message: `{err.reason}`\n')
-            raise MacheteException(
-                first_line + "Please open an issue regarding this topic under link: https://github.com/VirtusLab/git-machete/issues/new")
-    except OSError as e:
-        raise MacheteException(f'Could not connect to {url_prefix}: {e}')
-
-
-def create_pull_request(domain: str, org: str, repo: str,
-                        head: str, base: str, title: str, description: str, draft: bool) -> GitHubPullRequest:
-    token: Optional[str] = __get_github_token(domain)
-    request_body: Dict[str, Any] = {
-        'head': head,
-        'base': base,
-        'title': title,
-        'body': description,
-        'draft': draft
-    }
-    pr = __fire_github_api_request(domain, 'POST', f'/repos/{org}/{repo}/pulls', token, request_body)
-    return __parse_pr_json(pr)
-
-
-def add_assignees_to_pull_request(domain: str, org: str, repo: str, number: int, assignees: List[str]) -> None:
-    token: Optional[str] = __get_github_token(domain)
-    request_body: Dict[str, List[str]] = {
-        'assignees': assignees
-    }
-    # Adding assignees is only available via the Issues API, not PRs API.
-    __fire_github_api_request(domain, 'POST', f'/repos/{org}/{repo}/issues/{number}/assignees', token, request_body)
-
-
-def add_reviewers_to_pull_request(domain: str, org: str, repo: str, number: int, reviewers: List[str]) -> None:
-    token: Optional[str] = __get_github_token(domain)
-    request_body: Dict[str, List[str]] = {
-        'reviewers': reviewers
-    }
-    __fire_github_api_request(domain, 'POST', f'/repos/{org}/{repo}/pulls/{number}/requested_reviewers', token, request_body)
-
-
-def set_base_of_pull_request(domain: str, org: str, repo: str, number: int, base: LocalBranchShortName) -> None:
-    token: Optional[str] = __get_github_token(domain)
-    request_body: Dict[str, str] = {'base': base}
-    __fire_github_api_request(domain, 'PATCH', f'/repos/{org}/{repo}/pulls/{number}', token, request_body)
-
-
-def set_milestone_of_pull_request(domain: str, org: str, repo: str, number: int, milestone: str) -> None:
-    token: Optional[str] = __get_github_token(domain)
-    request_body: Dict[str, str] = {'milestone': milestone}
-    # Setting milestone is only available via the Issues API, not PRs API.
-    __fire_github_api_request(domain, 'PATCH', f'/repos/{org}/{repo}/issues/{number}', token, request_body)
-
-
-def derive_pull_request_by_head(domain: str, org: str, repo: str, head: LocalBranchShortName) -> Optional[GitHubPullRequest]:
-    token: Optional[str] = __get_github_token(domain)
-    prs = __fire_github_api_request(domain, 'GET', f'/repos/{org}/{repo}/pulls?head={org}:{head}', token)
-    if len(prs) >= 1:
-        return __parse_pr_json(prs[0])
-    else:
-        return None
-
-
-def derive_pull_requests(domain: str, org: str, repo: str) -> List[GitHubPullRequest]:
-    token: Optional[str] = __get_github_token(domain)
-    # As of Dec 2022, GitHub API never returns more than 100 PRs, even if per_page>100.
-    prs = __fire_github_api_request(domain, 'GET', f'/repos/{org}/{repo}/pulls?per_page=100', token)
-    return list(map(__parse_pr_json, prs))
-
-
-def derive_current_user_login(domain: str) -> Optional[str]:
-    token: Optional[str] = __get_github_token(domain)
-    if not token:
-        return None
-    user = __fire_github_api_request(domain, 'GET', '/user', token)
-    return str(user['login'])  # str() to satisfy mypy
-
-
-def is_github_remote_url(domain: str, url: str) -> bool:
-    return any((re.match(pattern, url) for pattern in github_remote_url_patterns(domain)))
-
-
-def get_parsed_github_remote_url(domain: str, url: str, remote: str) -> Optional[RemoteAndOrganizationAndRepository]:
-    for pattern in github_remote_url_patterns(domain):
-        match = re.match(pattern, url)
-        if match:
-            org = match.group(1)
-            repo = match.group(2)
-            return RemoteAndOrganizationAndRepository(remote=remote,
-                                                      organization=org,
-                                                      repository=repo if repo[-4:] != '.git' else repo[:-4])
-    return None
+            return None
 
+    def derive_pull_requests(self) -> List[GitHubPullRequest]:
+        # As of Dec 2022, GitHub API never returns more than 100 PRs, even if per_page>100.
+        prs = self.__fire_github_api_request(method='GET',
+                                             path=f'/repos/{self.__organization}/{self.__repository}/pulls?per_page=100')
+        return list(map(GitHubPullRequest.from_json, prs))
 
-def get_pull_request_by_number_or_none(domain: str, number: int, org: str, repo: str) -> Optional[GitHubPullRequest]:
-    token: Optional[str] = __get_github_token(domain)
-    try:
-        pr_json: Dict[str, Any] = __fire_github_api_request(domain, 'GET', f'/repos/{org}/{repo}/pulls/{number}', token)
-        return __parse_pr_json(pr_json)
-    except MacheteException:
-        return None
+    def derive_current_user_login(self) -> Optional[str]:
+        if not self.__token:
+            return None
+        user = self.__fire_github_api_request(method='GET',
+                                              path='/user')
+        return str(user['login'])  # str() to satisfy mypy
+
+    def get_pull_request_by_number_or_none(self,
+                                           number: int
+                                           ) -> Optional[GitHubPullRequest]:
+        try:
+            path = f'/repos/{self.__organization}/{self.__repository}/pulls/{number}'
+            pr_json: Dict[str, Any] = self.__fire_github_api_request(method='GET',
+                                                                     path=path)
+            return GitHubPullRequest.from_json(pr_json)
+        except MacheteException:
+            return None
 
+    def get_repo_and_org_names_by_id(self,
+                                     repo_id: str
+                                     ) -> str:
+        repo = self.__fire_github_api_request(path='GET',
+                                              method=f'/repositories/{repo_id}')
+        return str(repo['full_name'])
 
-def checkout_pr_refs(git: GitContext, remote: str, pr_number: int, branch: LocalBranchShortName) -> None:
-    git.fetch_ref(remote, f'pull/{pr_number}/head:{branch}')
-    git.checkout(branch)
+    @staticmethod
+    def checkout_pr_refs(git: GitContext,
+                         remote: str,
+                         pr_number: int,
+                         branch: LocalBranchShortName
+                         ) -> None:
+        git.fetch_ref(remote, f'pull/{pr_number}/head:{branch}')
+        git.checkout(branch)
 
 
-def get_repo_and_org_names_by_id(domain: str, repo_id: str) -> str:
-    token: Optional[str] = __get_github_token(domain)
-    repo = __fire_github_api_request(domain, 'GET', f'/repositories/{repo_id}', token)
-    return str(repo['full_name'])
+def github_remote_url_patterns(domain: str) -> List[str]:
+    # GitHub DOES NOT allow trailing `.git` suffix in the repository name (also applies to multiple repetitions e.g. `repo_name.git.git`)
+    domain_regex = re.escape(domain)
+    return [
+        f"^https://.*@{domain_regex}/(.*)/(.*)$",
+        f"^https://{domain_regex}/(.*)/(.*)$",
+        f"^git@{domain_regex}:(.*)/(.*)$",
+        f"^ssh://git@{domain_regex}/(.*)/(.*)$"
+    ]
 
 
-def get_github_token_possible_providers() -> str:
-    return (f'\n\t1. `{GITHUB_TOKEN_ENV_VAR}` environment variable.\n'
-            '\t2. Content of the `~/.github-token` file.\n'
-            '\t3. Current auth token from the `gh` GitHub CLI.\n'
-            '\t4. Current auth token from the `hub` GitHub CLI.\n')
+def is_github_remote_url(domain: str,
+                         url: str
+                         ) -> bool:
+    return any((re.match(pattern, url) for pattern in github_remote_url_patterns(domain)))
diff --git a/requirements.sphinx-docs.txt b/requirements.sphinx-docs.txt
index 2cc8302..0982f3d 100644
--- a/requirements.sphinx-docs.txt
+++ b/requirements.sphinx-docs.txt
@@ -1,2 +1,2 @@
-sphinx==5.3.0
-sphinx-rtd-theme==1.1.1
+sphinx==6.1.3
+sphinx-rtd-theme==1.2.0


Test patch:
diff --git a/requirements.testenv.txt b/requirements.testenv.txt
index ab56c6e..0536d0e 100644
--- a/requirements.testenv.txt
+++ b/requirements.testenv.txt
@@ -1,3 +1,3 @@
 pytest==7.2.1
 pytest-mock==3.10.0
-pytest-xdist==3.1.0
+pytest-xdist==3.2.0
diff --git a/tests/mockers.py b/tests/mockers.py
index 7fde6df..acdd3e0 100644
--- a/tests/mockers.py
+++ b/tests/mockers.py
@@ -16,7 +16,10 @@ from urllib.error import HTTPError
 from urllib.parse import ParseResult, parse_qs, urlparse
 
 from git_machete import cli
-from git_machete.git_operations import FullCommitHash, GitContext
+from git_machete.git_operations import (FullCommitHash, GitContext,
+                                        LocalBranchShortName)
+from git_machete.github import GitHubToken
+from git_machete.options import CommandLineOptions
 from git_machete.utils import dim
 
 """
@@ -345,6 +348,14 @@ class MockContextManager:
         pass
 
 
+class MockContextManagerRaise403(MockContextManager):
+    def __init__(self, obj: MockGitHubAPIResponse) -> None:
+        super().__init__(obj)
+
+    def __enter__(self) -> MockGitHubAPIResponse:
+        raise HTTPError(None, 403, 'Forbidden', None, None)
+
+
 def adapt(s: str, indent: str) -> str:
     return textwrap.indent(textwrap.dedent(s[1:]), indent)
 
@@ -422,3 +433,54 @@ def mock_exit_script(status_code: int, error: Optional[BaseException] = None) ->
 
 def mock_exit_script_no_exit(status_code: int, error: Optional[BaseException] = None) -> None:
     return
+
+
+class FakeCommandLineOptions(CommandLineOptions):
+    def __init__(self) -> None:
+        super().__init__()
+        self.opt_no_interactive_rebase: bool = True
+        self.opt_yes: bool = True
+
+
+class FakeGitHubToken(GitHubToken):
+    def __bool__(self) -> bool:
+        return True
+
+    @property
+    def value(self) -> Optional[str]:
+        return 'fake_token'
+
+    @property
+    def provider(self) -> Optional[str]:
+        return 'fake_provider'
+
+
+class EmptyGitHubToken(GitHubToken):
+    def __bool__(self) -> bool:
+        return False
+
+    @property
+    def value(self) -> Optional[str]:
+        return 'dummy_token'
+
+    @property
+    def provider(self) -> Optional[str]:
+        return 'dummy_provider'
+
+
+def mock_github_remote_url_patterns(domain: str) -> List[str]:
+    return ['(.*)/(.*)']
+
+
+def mock_fetch_ref(cls: Any, remote: str, ref: str) -> None:
+    branch: LocalBranchShortName = LocalBranchShortName.of(ref[ref.index(':') + 1:])
+    git.create_branch(branch, get_current_commit_hash(), switch_head=True)
+
+
+def mock_derive_current_user_login(domain: str) -> str:
+    return "very_complex_user_token"
+
+
+def mock_input(msg: str) -> str:
+    print(msg)
+    return '1'
diff --git a/tests/test_file.py b/tests/test_file.py
index e958934..c98e925 100644
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -1,6 +1,5 @@
 import os
 from pathlib import Path
-from typing import Any
 
 from .mockers import GitContext, GitRepositorySandbox, launch_command
 
@@ -20,7 +19,7 @@ class TestFile:
             .execute('git config user.name "Tester Test"')
         )
 
-    def test_file(self, mocker: Any) -> None:
+    def test_file(self) -> None:
         """
         Verify behaviour of a 'git machete file' command.
         """
diff --git a/tests/test_git_operations.py b/tests/test_git_operations.py
index 49184bd..0855bcf 100644
--- a/tests/test_git_operations.py
+++ b/tests/test_git_operations.py
@@ -1,4 +1,4 @@
-from unittest import mock
+from typing import Any
 
 from git_machete.git_operations import (FullCommitHash, GitContext,
                                         LocalBranchShortName)
@@ -23,11 +23,11 @@ class TestGitOperations:
             .execute('git config user.name "Tester Test"')
         )
 
-    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)
-    def test_run_git(self) -> None:
+    def test_run_git(self, mocker: Any) -> None:
         """
         Verify behaviour of a 'GitContext._run_git()' method
         """
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)
         (
             self.repo_sandbox.new_branch("master")
                 .commit("master first commit")
diff --git a/tests/test_github.py b/tests/test_github.py
index 97b98dd..137954f 100644
--- a/tests/test_github.py
+++ b/tests/test_github.py
@@ -2,57 +2,24 @@ import json
 import os
 import subprocess
 from tempfile import mkdtemp
-from typing import Any, Dict, List, Optional
-from unittest import mock
+from typing import Any, Dict
 
 import pytest
 
 from git_machete.exceptions import MacheteException
-from git_machete.git_operations import LocalBranchShortName
-from git_machete.github import get_parsed_github_remote_url
-from git_machete.options import CommandLineOptions
+from git_machete.github import GitHubClient, RemoteAndOrganizationAndRepository
 
-from .mockers import (GitRepositorySandbox, MockContextManager,
+from .mockers import (EmptyGitHubToken, FakeCommandLineOptions,
+                      FakeGitHubToken, GitRepositorySandbox,
+                      MockContextManager, MockContextManagerRaise403,
                       MockGitHubAPIState, MockHTTPError, assert_command,
-                      get_current_commit_hash, git, launch_command,
-                      mock_ask_if, mock_exit_script, mock_run_cmd,
+                      launch_command, mock_ask_if,
+                      mock_derive_current_user_login, mock_exit_script,
+                      mock_fetch_ref, mock_github_remote_url_patterns,
+                      mock_input, mock_run_cmd,
                       mock_should_perform_interactive_slide_out,
                       rewrite_definition_file)
 
-
-class FakeCommandLineOptions(CommandLineOptions):
-    def __init__(self) -> None:
-        super().__init__()
-        self.opt_no_interactive_rebase: bool = True
-        self.opt_yes: bool = True
-
-
-def mock_github_remote_url_patterns(domain: str) -> List[str]:
-    return ['(.*)/(.*)']
-
-
-def mock_fetch_ref(cls: Any, remote: str, ref: str) -> None:
-    branch: LocalBranchShortName = LocalBranchShortName.of(ref[ref.index(':') + 1:])
-    git.create_branch(branch, get_current_commit_hash(), switch_head=True)
-
-
-def mock_derive_current_user_login(domain: str) -> str:
-    return "very_complex_user_token"
-
-
-def mock__get_github_token_none(domain: str) -> Optional[str]:
-    return None
-
-
-def mock__get_github_token_fake(domain: str) -> Optional[str]:
-    return 'token'
-
-
-def mock_input(msg: str) -> str:
-    print(msg)
-    return '1'
-
-
 prs_per_page = 3
 number_of_pages = 3
 
@@ -132,10 +99,11 @@ class TestGithub:
         ]
     )
 
-    @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(self) -> None:
+    def test_github_retarget_pr(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_retarget_pr.new_request())
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+
         branchs_first_commit_msg = "First commit on branch."
         branchs_second_commit_msg = "Second commit on branch."
         (
@@ -191,12 +159,13 @@ class TestGithub:
             strip_indentation=False
         )
 
-    @mock.patch('git_machete.cli.exit_script', mock_exit_script)
-    @mock.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_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:
+    def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_retarget_pr.new_request())
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+
         branchs_first_commit_msg = "First commit on branch."
         branchs_second_commit_msg = "Second commit on branch."
 
@@ -323,11 +292,12 @@ class TestGithub:
         ]
     )
 
-    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-    @mock.patch('git_machete.github.derive_current_user_login', mock_derive_current_user_login)
-    @mock.patch('urllib.request.urlopen', MockContextManager)
-    @mock.patch('urllib.request.Request', git_api_state_for_test_anno_prs.new_request())
-    def test_github_anno_prs(self) -> None:
+    def test_github_anno_prs(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('git_machete.github.GitHubClient.derive_current_user_login', mock_derive_current_user_login)
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_anno_prs.new_request())
+
         (
             self.repo_sandbox.new_branch("root")
                 .commit("root")
@@ -446,16 +416,18 @@ class TestGithub:
         ]
     )
 
-    @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_url_patterns', mock_github_remote_url_patterns)
-    @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_create_pr.new_request())
-    @mock.patch('urllib.request.urlopen', MockContextManager)
-    def test_github_create_pr(self) -> None:
+    def test_github_create_pr(self, mocker: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+        mocker.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.
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('urllib.error.HTTPError', MockHTTPError)  # need to provide read() method, which does not actually reads error from url
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_create_pr.new_request())
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+
         (
             self.repo_sandbox.new_branch("root")
                 .commit("initial commit")
@@ -626,15 +598,17 @@ class TestGithub:
         ]
     )
 
-    # 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_url_patterns', mock_github_remote_url_patterns)
-    @mock.patch('git_machete.github.__get_github_token', mock__get_github_token_none)
-    @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('git_machete.client.MacheteClient.ask_if', mock_ask_if)
-    @mock.patch('urllib.request.urlopen', MockContextManager)
-    @mock.patch('urllib.request.Request', git_api_state_for_test_create_pr_missing_base_branch_on_remote.new_request())
-    def test_github_create_pr_missing_base_branch_on_remote(self) -> None:
+    def test_github_create_pr_missing_base_branch_on_remote(self, mocker: Any) -> None:
+        # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_create_pr`
+        # due to `git fetch` executed by `create-pr` subcommand.
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_create_pr_missing_base_branch_on_remote.new_request())
+
         (
             self.repo_sandbox.new_branch("root")
                 .commit("initial commit")
@@ -684,18 +658,20 @@ class TestGithub:
         ]
     )
 
-    @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_url_patterns', mock_github_remote_url_patterns)
-    @mock.patch('git_machete.github.__get_github_token', mock__get_github_token_none)
-    @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:
+    def test_github_create_pr_with_multiple_non_origin_remotes(self, mocker: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+        mocker.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.
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('urllib.error.HTTPError', MockHTTPError)  # need to provide read() method, which does not actually read error from url
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_create_pr_with_multiple_non_origin_remotes.new_request())
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('builtins.input', mock_input)
+
         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)
@@ -945,16 +921,17 @@ class TestGithub:
         ]
     )
 
-    @mock.patch('git_machete.cli.exit_script', mock_exit_script)
-    # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs`
-    # due to `git fetch` executed by `checkout-prs` subcommand.
-    @mock.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-    @mock.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
-    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-    @mock.patch('git_machete.github.__get_github_token', mock__get_github_token_none)
-    @mock.patch('urllib.request.Request', git_api_state_for_test_checkout_prs.new_request())
-    @mock.patch('urllib.request.urlopen', MockContextManager)
-    def test_github_checkout_prs(self, tmp_path: Any) -> None:
+    def test_github_checkout_prs(self, mocker: Any, tmp_path: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+        # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs`
+        # due to `git fetch` executed by `checkout-prs` subcommand.
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_checkout_prs.new_request())
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+
         (
             self.repo_sandbox.new_branch("root")
             .commit("initial commit")
@@ -1139,10 +1116,10 @@ class TestGithub:
         )
 
         # check against wrong pr number
-        repo: str
-        org: str
-        _, org, repo = get_parsed_github_remote_url(domain='github.com', url=self.repo_sandbox.remote_path, remote='origin')
-        expected_error_message = f"PR #100 is not found in repository {org}/{repo}"
+        remote_org_repo = RemoteAndOrganizationAndRepository.from_url(domain=GitHubClient.DEFAULT_GITHUB_DOMAIN,
+                                                                      url=self.repo_sandbox.remote_path,
+                                                                      remote='origin')
+        expected_error_message = f"PR #100 is not found in repository {remote_org_repo.organization}/{remote_org_repo.repository}"
         with pytest.raises(MacheteException) as e:
             launch_command('github', 'checkout-prs', '100')
         if e:
@@ -1157,7 +1134,8 @@ class TestGithub:
 
         # check against user with no open pull requests
         expected_msg = ("Checking for open GitHub PRs... OK\n"
-                        f"Warn: User tester has no open pull request in repository {org}/{repo}\n")
+                        f"Warn: User tester has no open pull request in repository "
+                        f"{remote_org_repo.organization}/{remote_org_repo.repository}\n")
         assert_command(['github', 'checkout-prs', '--by', 'tester'], expected_msg, strip_indentation=False)
 
         # Check against closed pull request with head branch deleted from remote
@@ -1240,14 +1218,15 @@ class TestGithub:
         ]
     )
 
-    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-    # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs_freshly_cloned`
-    # due to `git fetch` executed by `checkout-prs` subcommand.
-    @mock.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
-    @mock.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-    @mock.patch('urllib.request.urlopen', MockContextManager)
-    @mock.patch('urllib.request.Request', git_api_state_for_test_github_checkout_prs_fresh_repo.new_request())
-    def test_github_checkout_prs_freshly_cloned(self, tmp_path: Any) -> None:
+    def test_github_checkout_prs_freshly_cloned(self, mocker: Any, tmp_path: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs_freshly_cloned`
+        # due to `git fetch` executed by `checkout-prs` subcommand.
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_checkout_prs_fresh_repo.new_request())
+
         (
             self.repo_sandbox.new_branch("root")
             .commit("initial commit")
@@ -1363,16 +1342,17 @@ class TestGithub:
         ]
     )
 
-    @mock.patch('git_machete.git_operations.GitContext.fetch_ref', mock_fetch_ref)
-    # need to mock fetch_ref due to underlying `git fetch pull/head` calls
-    @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('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-    # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs_from_fork_with_deleted_repo`
-    # due to `git fetch` executed by `checkout-prs` subcommand.
-    @mock.patch('urllib.request.urlopen', MockContextManager)
-    @mock.patch('urllib.request.Request', git_api_state_for_test_github_checkout_prs_from_fork_with_deleted_repo.new_request())
-    def test_github_checkout_prs_from_fork_with_deleted_repo(self) -> None:
+    def test_github_checkout_prs_from_fork_with_deleted_repo(self, mocker: Any) -> None:
+        mocker.patch('git_machete.git_operations.GitContext.fetch_ref', mock_fetch_ref)
+        # need to mock fetch_ref due to underlying `git fetch pull/head` calls
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs_from_fork_with_deleted_repo`
+        # due to `git fetch` executed by `checkout-prs` subcommand.
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_checkout_prs_from_fork_with_deleted_repo.new_request())
+
         (
             self.repo_sandbox.new_branch("root")
             .commit('initial master commit')
@@ -1475,17 +1455,19 @@ class TestGithub:
         ]
     )
 
-    @mock.patch('git_machete.cli.exit_script', mock_exit_script)
-    # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs`
-    # due to `git fetch` executed by `checkout-prs` subcommand.
-    @mock.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-    @mock.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
-    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-    @mock.patch('git_machete.github.__get_github_token', mock__get_github_token_none)
-    @mock.patch('urllib.request.Request', git_api_state_for_test_github_checkout_prs_of_current_user_and_other_users.new_request())
-    @mock.patch('urllib.request.urlopen', MockContextManager)
-    @mock.patch('git_machete.github.derive_current_user_login', mock_derive_current_user_login)
-    def test_github_checkout_prs_of_current_user_and_other_users(self, tmp_path: Any) -> None:
+    def test_github_checkout_prs_of_current_user_and_other_users(self, mocker: Any, tmp_path: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+        # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs`
+        # due to `git fetch` executed by `checkout-prs` subcommand.
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('urllib.request.Request',
+                     self.git_api_state_for_test_github_checkout_prs_of_current_user_and_other_users.new_request())
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('git_machete.github.GitHubClient.derive_current_user_login', mock_derive_current_user_login)
+
         (
             self.repo_sandbox.new_branch("root")
             .commit("initial commit")
@@ -1627,15 +1609,16 @@ class TestGithub:
         ]
     )
 
-    @mock.patch('git_machete.client.MacheteClient.should_perform_interactive_slide_out', mock_should_perform_interactive_slide_out)
-    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)
-    @mock.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
-    @mock.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
-    @mock.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-    @mock.patch('git_machete.github.__get_github_token', mock__get_github_token_fake)
-    @mock.patch('urllib.request.urlopen', MockContextManager)
-    @mock.patch('urllib.request.Request', git_api_state_for_test_github_sync.new_request())
-    def test_github_sync(self) -> None:
+    def test_github_sync(self, mocker: Any) -> None:
+        mocker.patch('git_machete.client.MacheteClient.should_perform_interactive_slide_out', mock_should_perform_interactive_slide_out)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)
+        mocker.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.github.GitHubToken', FakeGitHubToken)
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_sync.new_request())
+
         (
             self.repo_sandbox
                 .new_branch('master')
@@ -1698,23 +1681,26 @@ class TestGithub:
         urls = urls + [url + '.git' for url in urls]
 
         for url in urls:
-            remote_and_organization_and_repository = get_parsed_github_remote_url('github.com', url, 'origin')
+            remote_and_organization_and_repository = RemoteAndOrganizationAndRepository.from_url(domain=GitHubClient.DEFAULT_GITHUB_DOMAIN,
+                                                                                                 url=url,
+                                                                                                 remote='origin')
             assert remote_and_organization_and_repository.organization == organization
             assert remote_and_organization_and_repository.repository == repository
 
-    @mock.patch('git_machete.cli.exit_script', mock_exit_script)
-    # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs`
-    # due to `git fetch` executed by `checkout-prs` subcommand.
-    @mock.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-    @mock.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
-    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-    @mock.patch('git_machete.github.__get_github_token', mock__get_github_token_none)
-    @mock.patch('urllib.request.urlopen', MockContextManager)
-    @mock.patch('git_machete.github.derive_current_user_login', mock_derive_current_user_login)
-    @mock.patch('urllib.request.Request', MockGitHubAPIState([]).new_request())
-    @mock.patch('tests.mockers.MockGitHubAPIResponse.info', mock_info)
-    @mock.patch('tests.mockers.MockGitHubAPIResponse.read', mock_read)
-    def test_github_api_pagination(self, tmp_path: Any) -> None:
+    def test_github_api_pagination(self, mocker: Any, tmp_path: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+        # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs`
+        # due to `git fetch` executed by `checkout-prs` subcommand.
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('git_machete.github.GitHubClient.derive_current_user_login', mock_derive_current_user_login)
+        mocker.patch('urllib.request.Request', MockGitHubAPIState([]).new_request())
+        mocker.patch('tests.mockers.MockGitHubAPIResponse.info', mock_info)
+        mocker.patch('tests.mockers.MockGitHubAPIResponse.read', mock_read)
+
         global prs_per_page
         (
             self.repo_sandbox.new_branch("develop")
@@ -1741,3 +1727,65 @@ class TestGithub:
         expected_status_output = '  develop *\n' + '\n'.join([f' |\n o-feature_{i}  rebase=no push=no'
                                                               for i in range(number_of_pages * prs_per_page)]) + '\n'
         assert_command(['status'], expected_status_output)
+
+    def test_github_enterprise_domain_fail(self, mocker: Any) -> None:
+        mocker.patch('git_machete.client.MacheteClient.should_perform_interactive_slide_out', mock_should_perform_interactive_slide_out)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)
+        mocker.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('urllib.request.urlopen', MockContextManagerRaise403)
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+
+        github_enterprise_domain = 'git.example.org'
+        self.repo_sandbox.add_git_config_key('machete.github.domain', github_enterprise_domain)
+
+        expected_error_message = (
+            "GitHub API returned `403` HTTP status with error message: `Forbidden`\n"
+            "You might not have the required permissions for this repository.\n"
+            "Provide a GitHub API token with `repo` access via dummy_provider.\n"
+            f"Visit `https://{github_enterprise_domain}/settings/tokens` to generate a new one.\n"
+            "You can also use a different token provider, available providers can be found when running `git machete help github`.")
+
+        with pytest.raises(MacheteException) as e:
+            launch_command('github', 'checkout-prs', '--all')
+        if e:
+            assert e.value.args[0] == expected_error_message, 'Verify that expected error message has appeared.'
+
+    def test_github_enterprise_domain(self, mocker: Any) -> None:
+        git_api_state_for_test_github_enterprise_domain = MockGitHubAPIState(
+            [
+                {
+                    'head': {'ref': 'snickers', 'repo': self.mock_repository_info},
+                    'user': {'login': 'other_user'},
+                    'base': {'ref': 'develop'},
+                    'number': '7',
+                    'html_url': 'www.github.com',
+                    'state': 'open'
+                }
+            ]
+        )
+        mocker.patch('git_machete.client.MacheteClient.should_perform_interactive_slide_out', mock_should_perform_interactive_slide_out)
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)
+        mocker.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
+        mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('git_machete.github.GitHubToken', FakeGitHubToken)
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.Request', git_api_state_for_test_github_enterprise_domain.new_request())
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+
+        github_enterprise_domain = 'git.example.org'
+        (
+            self.repo_sandbox.new_branch("develop")
+            .commit("first commit")
+            .push()
+            .new_branch("snickers")
+            .commit("first commit")
+            .push()
+            .check_out("develop")
+            .delete_branch("snickers")
+            .add_git_config_key('machete.github.domain', github_enterprise_domain)
+        )
+        launch_command('github', 'checkout-prs', '--all')
diff --git a/tests/test_help.py b/tests/test_help.py
index 67e21b9..90482da 100644
--- a/tests/test_help.py
+++ b/tests/test_help.py
@@ -1,5 +1,4 @@
 from typing import Any, List
-from unittest import mock
 
 import pytest
 
@@ -58,8 +57,8 @@ class TestHelp:
                     f"Verify that `git machete {command} --help` causes " \
                     f"SystemExit with {ExitCode.ARGUMENT_ERROR} exit code."
 
-    @mock.patch('git_machete.cli.exit_script', mock_exit_script_no_exit)
-    def test_help_output_has_no_ansi_codes(self) -> None:
+    def test_help_output_has_no_ansi_codes(self, mocker: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script_no_exit)
         for command in commands_and_aliases:
             help_output = launch_command('help', command)
             assert '\033' not in help_output