Repository: VirtusLab/git-machete
Pull request #1368 (2024-11-06 12:46:37)

Cache code hosting client and open PRs/MRs as a attributes of `MacheteClient`

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

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


```diff
@@             Coverage Diff             @@
##           develop    #1368      +/-   ##
===========================================
- Coverage    98.46%   98.45%   -0.01%     
===========================================
  Files           14       14              
  Lines         3968     3956      -12     
  Branches       780      778       -2     
===========================================
- Hits          3907     3895      -12     
  Misses          39       39              
  Partials        22       22              
```

</details>

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


Patch:
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index ef28221..f3d4493 100644
--- a/docs/man/git-machete.1
+++ b/docs/man/git-machete.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "GIT-MACHETE" "1" "Nov 05, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Nov 06, 2024" "" "git-machete"
 .SH NAME
 git-machete \- git-machete 3.31.0
 .sp
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 1b9e86e..48060b1 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -814,7 +814,7 @@ def launch(orig_args: List[str]) -> None:
                         f"`update-{pr_or_mr}-descriptions` subcommand must take exactly one of the following options: "
                         '`--all`, `--mine`, `--related`')
                 machete_client.update_pull_request_descriptions(
-                    spec, all_open_prs_preloaded=None, all=cli_opts.opt_all, mine=cli_opts.opt_mine, related=cli_opts.opt_related)
+                    spec, all=cli_opts.opt_all, mine=cli_opts.opt_mine, related=cli_opts.opt_related)
             else:  # an unknown subcommand is handled by argparse
                 raise UnexpectedMacheteException(f"Unknown subcommand: `{subcommand}`")
         elif cmd == "is-managed":
diff --git a/git_machete/client.py b/git_machete/client.py
index db54483..695c5d7 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -120,6 +120,22 @@ class MacheteClient:
         self.__annotations: Dict[LocalBranchShortName, Annotation] = {}
         self.__empty_line_status: Optional[bool] = None
         self.__branch_pairs_by_hash_in_reflog: Optional[Dict[FullCommitHash, List[BranchPair]]] = None
+        self.__all_open_prs: Optional[List[PullRequest]] = None
+        self.__code_hosting_client: Optional[CodeHostingClient] = None
+
+    @property
+    def code_hosting_spec(self) -> CodeHostingSpec:
+        return self.code_hosting_client._spec
+
+    @property
+    def code_hosting_client(self) -> CodeHostingClient:
+        if self.__code_hosting_client is None:
+            raise UnexpectedMacheteException("Code hosting client has not been initialized, this is an unexpected state.")
+        return self.__code_hosting_client
+
+    @code_hosting_client.setter
+    def code_hosting_client(self, value: CodeHostingClient) -> None:
+        self.__code_hosting_client = value
 
     @property
     def branch_layout_file_path(self) -> str:
@@ -1702,16 +1718,19 @@ class MacheteClient:
               f"and branch reset events irrelevant for fork point/upstream inference): {reflog}")
         return result
 
+    def get_all_open_prs(self) -> List[PullRequest]:
+        if self.__all_open_prs is None:
+            spec = self.code_hosting_spec
+            print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True)
+            self.__all_open_prs = self.code_hosting_client.get_open_pull_requests()
+            print(fmt('<green><b>OK</b></green>'))
+        return self.__all_open_prs
+
     def sync_annotations_to_prs(self, spec: CodeHostingSpec, include_urls: bool) -> None:
-        domain = self.__derive_code_hosting_domain(spec)
-        org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain)
-        code_hosting_client = spec.create_client(
-            domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
-        print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True)
-        current_user: Optional[str] = code_hosting_client.get_current_user_login()
+        self.__init_code_hosting_client(spec)
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
         debug(f'Current {spec.display_name} user is ' + (bold(current_user or '<none>')))
-        all_open_prs: List[PullRequest] = code_hosting_client.get_open_pull_requests()
-        print(fmt('<green><b>OK</b></green>'))
+        all_open_prs = self.get_all_open_prs()
         self.__sync_annotations_to_branch_layout_file(spec, all_open_prs, current_user, include_urls=include_urls, verbose=True)
 
     def __pull_request_annotation(self, spec: CodeHostingSpec,
@@ -2166,44 +2185,29 @@ class MacheteClient:
                 f"Fork point {bold(fork_point_hash)} is not ancestor of or the tip "
                 f"of the {bold(branch)} branch.")
 
-    def update_pull_request_descriptions(self,
-                                         spec: CodeHostingSpec,
-                                         all_open_prs_preloaded: Optional[List[PullRequest]],
-                                         *,
-                                         all: bool = False,
-                                         mine: bool = False,
-                                         related: bool = False,
-                                         ) -> None:
-        domain = self.__derive_code_hosting_domain(spec)
-        org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain)
-        code_hosting_client = spec.create_client(domain=domain, organization=org_repo_remote.organization,
-                                                 repository=org_repo_remote.repository)
+    def update_pull_request_descriptions(self, spec: CodeHostingSpec,
+                                         *, all: bool = False, mine: bool = False, related: bool = False) -> None:
+        if self.__code_hosting_client is None:
+            self.__init_code_hosting_client(spec)
 
-        current_user: Optional[str] = code_hosting_client.get_current_user_login()
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
         if not current_user and mine:
             msg = (f"Could not determine current user name, please check that the {spec.display_name} API token provided by one of the: "
                    f"{spec.token_providers_message}is valid.")
             raise MacheteException(msg)
-        if all_open_prs_preloaded is not None:
-            all_open_prs = all_open_prs_preloaded
-        else:
-            print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True)
-            all_open_prs = code_hosting_client.get_open_pull_requests()
-            print(fmt('<green><b>OK</b></green>'))
 
         if related:
             head = self.__git.get_current_branch()
-            current_pr = self.__get_sole_pull_request_for_head(code_hosting_client, head, ignore_if_missing=False)
+            current_pr = self.__get_sole_pull_request_for_head(head, ignore_if_missing=False)
         else:
             current_pr = None
         applicable_prs: List[PullRequest] = self.__get_applicable_pull_requests(
-            pr_numbers=[], all_open_prs=all_open_prs, code_hosting_client=code_hosting_client,
-            all=all, mine=mine, by=None, related_to=current_pr, user=current_user)
+            pr_numbers=[], all=all, mine=mine, by=None, related_to=current_pr, user=current_user)
 
         for pr in applicable_prs:
-            new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, all_open_prs_preloaded=all_open_prs)
+            new_description = self.__get_updated_pull_request_description(pr)
             if pr.description != new_description:
-                code_hosting_client.set_description_of_pull_request(pr.number, description=new_description)
+                self.code_hosting_client.set_description_of_pull_request(pr.number, description=new_description)
                 print(fmt(f'Description of {pr.display_text()} (<b>{pr.head} {get_right_arrow()} {pr.base}</b>) has been updated'))
 
     def checkout_pull_requests(self,
@@ -2215,12 +2219,9 @@ class MacheteClient:
                                by: Optional[str] = None,
                                fail_on_missing_current_user_for_my_open_prs: bool = False
                                ) -> None:
-        domain = self.__derive_code_hosting_domain(spec)
-        org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain)
-        code_hosting_client = spec.create_client(domain=domain, organization=org_repo_remote.organization,
-                                                 repository=org_repo_remote.repository)
+        domain, org_repo_remote = self.__init_code_hosting_client(spec)
 
-        current_user: Optional[str] = code_hosting_client.get_current_user_login()
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
         if not current_user and mine:
             msg = (f"Could not determine current user name, please check that the {spec.display_name} API token provided by one of the: "
                    f"{spec.token_providers_message}is valid.")
@@ -2229,20 +2230,16 @@ class MacheteClient:
             else:
                 warn(msg)
                 return
-        print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True)
-        all_open_prs: List[PullRequest] = code_hosting_client.get_open_pull_requests()
-        print(fmt('<green><b>OK</b></green>'))
 
         applicable_prs: List[PullRequest] = self.__get_applicable_pull_requests(
-            pr_numbers, all_open_prs=all_open_prs, code_hosting_client=code_hosting_client,
-            all=all, mine=mine, by=by, related_to=None, user=current_user)
+            pr_numbers, all=all, mine=mine, by=by, related_to=None, user=current_user)
 
         debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}')
         self.__git.fetch_remote(org_repo_remote.remote)
 
         prs_to_annotate = set(applicable_prs)
         for pr in sorted(applicable_prs, key=lambda x: x.number):
-            head_org_repo_and_git_url = code_hosting_client.get_org_repo_and_git_url_by_repo_id_or_none(pr.head_repo_id)
+            head_org_repo_and_git_url = self.code_hosting_client.get_org_repo_and_git_url_by_repo_id_or_none(pr.head_repo_id)
             if head_org_repo_and_git_url:
                 head_org = head_org_repo_and_git_url.organization  # explicit access to attributes to satisfy vulture
                 head_repo = head_org_repo_and_git_url.repository
@@ -2265,14 +2262,14 @@ class MacheteClient:
             else:
                 warn(f'{pr.display_text()} comes from fork and its {spec.repository_name} is already deleted. '
                      f'No remote tracking data will be set up for {bold(pr.head)} branch.')
-                refspec = f'{code_hosting_client.get_ref_name_for_pull_request(pr.number)}:{pr.head}'
+                refspec = f'{self.code_hosting_client.get_ref_name_for_pull_request(pr.number)}:{pr.head}'
                 self.__git.fetch_refspec(org_repo_remote.remote, refspec)
                 self.__git.checkout(LocalBranchShortName.of(pr.head))
             if pr.state in ('closed', 'merged'):
                 warn(f'{pr.display_text()} is already closed.')
             debug(f'found {pr}')
 
-            pr_path: List[PullRequest] = self.__get_upwards_path_including_pr(spec, pr, all_open_prs)
+            pr_path: List[PullRequest] = self.__get_upwards_path_including_pr(pr)
             prs_to_annotate.update(pr_path)
             reversed_pr_path: List[PullRequest] = pr_path[::-1]  # need to add from root downwards
             if reversed_pr_path[0].base not in self.managed_branches:
@@ -2302,16 +2299,14 @@ class MacheteClient:
         if len(applicable_prs) == 1:
             self.__git.checkout(LocalBranchShortName.of(applicable_prs[0].head))
 
-    @staticmethod
-    def __get_downwards_tree_excluding_pr(original_pr: PullRequest,
-                                          all_open_prs: List[PullRequest]) -> List[Tuple[PullRequest, int]]:
+    def __get_downwards_tree_excluding_pr(self, original_pr: PullRequest) -> List[Tuple[PullRequest, int]]:
         """Returns pairs of (PR, depth below the given PR)"""
 
         visited_head_branches: Set[str] = set([])
 
         def reverse_pr_dfs(pr: PullRequest, depth: int) -> Iterator[Tuple[PullRequest, int]]:
             visited_head_branches.add(pr.head)
-            down_prs = filter(lambda x: x.base == pr.head, all_open_prs)
+            down_prs = filter(lambda x: x.base == pr.head, self.get_all_open_prs())
             for down_pr in sorted(down_prs, key=lambda x: x.number):
                 if down_pr.head not in visited_head_branches:
                     yield (down_pr, depth + 1)
@@ -2319,9 +2314,7 @@ class MacheteClient:
 
         return list(reverse_pr_dfs(original_pr, 0))
 
-    @staticmethod
-    def __get_upwards_path_including_pr(spec: CodeHostingSpec, original_pr: PullRequest,
-                                        all_open_prs: List[PullRequest]) -> List[PullRequest]:
+    def __get_upwards_path_including_pr(self, original_pr: PullRequest) -> List[PullRequest]:
         visited_head_branches: List[str] = [original_pr.head]
         path: List[PullRequest] = [original_pr]
         pr_base: Optional[str] = original_pr.base
@@ -2332,10 +2325,11 @@ class MacheteClient:
             if pr_base in ('main', 'master'):
                 return path
             if pr_base in visited_head_branches:
+                spec = self.code_hosting_spec
                 raise MacheteException(f"There is a cycle between {spec.display_name} {spec.pr_short_name}s: " +
                                        " -> ".join(visited_head_branches + [pr_base]))
             visited_head_branches += [pr_base]
-            pr = utils.find_or_none(lambda x: x.head == pr_base, all_open_prs)
+            pr = utils.find_or_none(lambda x: x.head == pr_base, self.get_all_open_prs())
             path = (path + [pr]) if pr else path
             pr_base = pr.base if pr else None
         return path
@@ -2343,8 +2337,6 @@ class MacheteClient:
     def __get_applicable_pull_requests(
             self,
             pr_numbers: Optional[List[int]],
-            all_open_prs: List[PullRequest],
-            code_hosting_client: CodeHostingClient,
             all: bool,
             mine: bool,
             related_to: Optional[PullRequest],
@@ -2352,48 +2344,46 @@ class MacheteClient:
             user: Optional[str]
     ) -> List[PullRequest]:
         result: List[PullRequest] = []
-        spec = code_hosting_client._spec
+        spec = self.code_hosting_spec
+        all_open_prs = self.get_all_open_prs()
+        repo_pretty = f"{spec.repository_name} {bold(self.code_hosting_client.organization)}/{bold(self.code_hosting_client.repository)}"
         if pr_numbers:
             for pr_number in pr_numbers:
                 pr: Optional[PullRequest] = utils.find_or_none(lambda x: x.number == pr_number, all_open_prs)
                 if pr:
                     result.append(pr)
                 else:
-                    pr = code_hosting_client.get_pull_request_by_number_or_none(pr_number)
+                    pr = self.code_hosting_client.get_pull_request_by_number_or_none(pr_number)
                     if pr:
                         result.append(pr)
                     else:
 
-                        raise MacheteException(f"{spec.pr_short_name} {spec.pr_ordinal_char}{bold(str(pr_number))} "
-                                               f"is not found in {spec.repository_name} "
-                                               f"{bold(code_hosting_client.organization)}/{bold(code_hosting_client.repository)}")
+                        raise MacheteException(
+                            f"{spec.pr_short_name} {spec.pr_ordinal_char}{bold(str(pr_number))} is not found in {repo_pretty}")
             return result
         if all:
             if not all_open_prs:
-                warn(f"Currently there are no {spec.pr_full_name}s opened in {spec.repository_name} "
-                     f"{bold(code_hosting_client.organization)}/{bold(code_hosting_client.repository)}")
+                warn(f"Currently there are no {spec.pr_full_name}s opened in {repo_pretty}")
                 return []
             return all_open_prs
         elif mine and user:
             result = [pr for pr in all_open_prs if pr.user == user]
             if not result:
-                warn(f"Current user {bold(user)} has no open {spec.pr_full_name} in {spec.repository_name} "
-                     f"{bold(code_hosting_client.organization)}/{bold(code_hosting_client.repository)}")
+                warn(f"Current user {bold(user)} has no open {spec.pr_full_name} in {repo_pretty}")
                 return []
             return result
         elif by:
             result = [pr for pr in all_open_prs if pr.user == by]
             if not result:
-                warn(f"User {bold(by)} has no open {spec.pr_full_name} in {spec.repository_name} "
-                     f"{bold(code_hosting_client.organization)}/{bold(code_hosting_client.repository)}")
+                warn(f"User {bold(by)} has no open {spec.pr_full_name} in {repo_pretty}")
                 return []
             return result
         elif related_to:
-            style = self.__get_pr_description_into_style_from_config(spec)
+            style = self.__get_pr_description_into_style_from_config()
             result = []
             if style == PRDescriptionIntroStyle.FULL:
-                result += reversed(self.__get_upwards_path_including_pr(spec, related_to, all_open_prs))
-            result += [pr_ for pr_, _ in self.__get_downwards_tree_excluding_pr(related_to, all_open_prs)]
+                result += reversed(self.__get_upwards_path_including_pr(related_to))
+            result += [pr_ for pr_, _ in self.__get_downwards_tree_excluding_pr(related_to)]
             return result
 
         raise UnexpectedMacheteException("All params passed to __get_applicable_pull_requests are empty.")
@@ -2414,13 +2404,10 @@ class MacheteClient:
         return None
 
     def restack_pull_request(self, spec: CodeHostingSpec) -> None:
-        domain = self.__derive_code_hosting_domain(spec)
         head = self.__git.get_current_branch()
-        org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain, branch_used_for_tracking_data=head)
-        code_hosting_client = spec.create_client(
-            domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
+        _, org_repo_remote = self.__init_code_hosting_client(spec, branch_used_for_tracking_data=head)
 
-        pr: Optional[PullRequest] = self.__get_sole_pull_request_for_head(code_hosting_client, head, ignore_if_missing=False)
+        pr: Optional[PullRequest] = self.__get_sole_pull_request_for_head(head, ignore_if_missing=False)
         assert pr is not None
 
         self.__git.fetch_remote(org_repo_remote.remote)
@@ -2439,7 +2426,7 @@ class MacheteClient:
                     f'Branch <b>{current_branch}</b> is marked as `push=no`; aborting the restack.\n'
                     f'Did you want to just use `git machete {spec.git_machete_command} {subcommand}`?\n')
 
-            converted_to_draft = code_hosting_client.set_draft_status_of_pull_request(pr.number, target_draft_status=True)
+            converted_to_draft = self.code_hosting_client.set_draft_status_of_pull_request(pr.number, target_draft_status=True)
             if converted_to_draft:
                 print(f'{pr.display_text()} has been temporarily marked as draft')
 
@@ -2482,7 +2469,7 @@ class MacheteClient:
             self.__print_new_line(False)
 
             if converted_to_draft:
-                code_hosting_client.set_draft_status_of_pull_request(pr.number, target_draft_status=False)
+                self.code_hosting_client.set_draft_status_of_pull_request(pr.number, target_draft_status=False)
                 print(f'{pr.display_text()} has been marked as ready for review again')
 
         else:
@@ -2498,14 +2485,13 @@ class MacheteClient:
 
             self.retarget_pr(spec, head, ignore_if_missing=False)
 
-    def __get_updated_pull_request_description(self, code_hosting_client: CodeHostingClient, pr: PullRequest,
-                                               all_open_prs_preloaded: Optional[List[PullRequest]]) -> str:
+    def __get_updated_pull_request_description(self, pr: PullRequest) -> str:
         def skip_leading_empty(strs: List[str]) -> List[str]:
             return list(itertools.dropwhile(lambda line: line.strip() == '', strs))
 
         lines = skip_leading_empty(pr.description.splitlines()) if pr.description else []
-        style = self.__get_pr_description_into_style_from_config(code_hosting_client._spec)
-        text_to_prepend = self.__generate_pr_description_intro(code_hosting_client, pr, style, all_open_prs_preloaded)
+        style = self.__get_pr_description_into_style_from_config()
+        text_to_prepend = self.__generate_pr_description_intro(pr, style)
         lines_to_prepend = text_to_prepend.splitlines() if text_to_prepend else []
         if self.START_GIT_MACHETE_GENERATED_COMMENT in lines and self.END_GIT_MACHETE_GENERATED_COMMENT in lines:
             start_index = lines.index(self.START_GIT_MACHETE_GENERATED_COMMENT)
@@ -2520,12 +2506,9 @@ class MacheteClient:
         return '\n'.join(lines)
 
     def retarget_pr(self, spec: CodeHostingSpec, head: LocalBranchShortName, ignore_if_missing: bool) -> None:
-        domain = self.__derive_code_hosting_domain(spec)
-        org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain, branch_used_for_tracking_data=head)
-        code_hosting_client = spec.create_client(
-            domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
+        self.__init_code_hosting_client(spec, branch_used_for_tracking_data=head)
 
-        pr: Optional[PullRequest] = self.__get_sole_pull_request_for_head(code_hosting_client, head, ignore_if_missing=ignore_if_missing)
+        pr: Optional[PullRequest] = self.__get_sole_pull_request_for_head(head, ignore_if_missing=ignore_if_missing)
         if pr is None:
             return
 
@@ -2538,18 +2521,18 @@ class MacheteClient:
                 f' so that {bold(head)} is a child of {bold(pr.base)}.')
 
         if pr.base != new_base:
-            code_hosting_client.set_base_of_pull_request(pr.number, base=new_base)
+            self.code_hosting_client.set_base_of_pull_request(pr.number, base=new_base)
             print(f'{spec.base_branch_name.capitalize()} branch of {pr.display_text()} has been switched to {bold(new_base)}')
             pr = pr._replace(base=new_base)
         else:
             print(f'{spec.base_branch_name.capitalize()} branch of {pr.display_text()} is already {bold(new_base)}')
 
-        new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, all_open_prs_preloaded=None)
+        new_description = self.__get_updated_pull_request_description(pr)
         if pr.description != new_description:
-            code_hosting_client.set_description_of_pull_request(pr.number, description=new_description)
+            self.code_hosting_client.set_description_of_pull_request(pr.number, description=new_description)
             print(f'Description of {pr.display_text()} has been updated')
 
-        current_user: Optional[str] = code_hosting_client.get_current_user_login()
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
         if self.__annotations.get(head) and self.__annotations[head].qualifiers_text:
             self.__annotations[head] = Annotation(self.__pull_request_annotation(spec, pr, current_user) + ' ' +
                                                   self.__annotations[head].qualifiers_text)
@@ -2649,72 +2632,71 @@ class MacheteClient:
             f'You can select the {spec.repository_name} by providing some or all of git config keys:\n'
             f'{spec.git_config_keys.for_locating_repo_message()}\n')
 
+    def __init_code_hosting_client(self, spec: CodeHostingSpec,
+                                   branch_used_for_tracking_data: Optional[LocalBranchShortName] = None
+                                   ) -> Tuple[str, OrganizationAndRepositoryAndRemote]:
+        domain = self.__derive_code_hosting_domain(spec)
+        org_repo_remote = self.__derive_org_repo_and_remote(
+            spec, domain=domain, branch_used_for_tracking_data=branch_used_for_tracking_data)
+        self.code_hosting_client = spec.create_client(
+            domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
+        return domain, org_repo_remote
+
     START_GIT_MACHETE_GENERATED_COMMENT = '<!-- start git-machete generated -->'
     END_GIT_MACHETE_GENERATED_COMMENT = '<!-- end git-machete generated -->'
 
-    @staticmethod
     def __get_sole_pull_request_for_head(
-            code_hosting_client: CodeHostingClient,
+            self,
             head: LocalBranchShortName,
             ignore_if_missing: bool
     ) -> Optional[PullRequest]:
-        prs: List[PullRequest] = code_hosting_client.get_open_pull_requests_by_head(head)
-        spec = code_hosting_client._spec
+        prs: List[PullRequest] = self.code_hosting_client.get_open_pull_requests_by_head(head)
+        spec = self.code_hosting_spec
+        org_and_repo = self.code_hosting_client.get_org_and_repo()
         if not prs:
             if ignore_if_missing:
-                warn(f"no {spec.pr_short_name}s in <b>{code_hosting_client.get_org_and_repo()}</b> "
+                warn(f"no {spec.pr_short_name}s in <b>{org_and_repo}</b> "
                      f"have <b>{head}</b> as its {spec.head_branch_name} branch")
                 return None
             else:
-                raise MacheteException(f"No {spec.pr_short_name}s in <b>{code_hosting_client.get_org_and_repo()}</b> "
+                raise MacheteException(f"No {spec.pr_short_name}s in <b>{org_and_repo}</b> "
                                        f"have <b>{head}</b> as its {spec.head_branch_name} branch")
         if len(prs) > 1:
-            raise MacheteException(f"Multiple {spec.pr_short_name}s in <b>{code_hosting_client.get_org_and_repo()}</b> "
+            raise MacheteException(f"Multiple {spec.pr_short_name}s in <b>{org_and_repo}</b> "
                                    f"have <b>{head}</b> as its {spec.head_branch_name} branch: " +
                                    ", ".join(_pr.short_display_text() for _pr in prs))
         pr = prs[0]
         debug(f'found {pr}')
         return pr
 
-    def __get_pr_description_into_style_from_config(self, spec: CodeHostingSpec) -> PRDescriptionIntroStyle:
-        config_key = spec.git_config_keys.pr_description_intro_style
+    def __get_pr_description_into_style_from_config(self) -> PRDescriptionIntroStyle:
+        config_key = self.code_hosting_spec.git_config_keys.pr_description_intro_style
         return PRDescriptionIntroStyle.from_string(
             value=self.__git.get_config_attr(key=config_key, default_value="up-only"),
             from_where=f"`{config_key}` git config key"
         )
 
-    def __generate_pr_description_intro(self, code_hosting_client: CodeHostingClient,
-                                        pr: PullRequest, style: PRDescriptionIntroStyle,
-                                        all_open_prs_preloaded: Optional[List[PullRequest]]) -> str:
+    def __generate_pr_description_intro(self, pr: PullRequest, style: PRDescriptionIntroStyle) -> str:
         if style == PRDescriptionIntroStyle.NONE:
             return ''
 
-        if all_open_prs_preloaded is not None:
-            prs_for_base_branch = list(filter(lambda _pr: _pr.head == pr.base, all_open_prs_preloaded))
+        if self.__all_open_prs is not None:
+            prs_for_base_branch = list(filter(lambda _pr: _pr.head == pr.base, self.__all_open_prs))
         else:
             # For determining the PR chain, we need to fetch all PRs from the repo.
             # We could just fetch them straight away... but this list can be quite long for commercial monorepos,
             # esp. given that GitHub and GitLab limit the single page to 100 PRs/MRs (so multiple HTTP requests may be needed).
             # As a slight optimization, in the default UP_ONLY style,
             # let's fetch the full PR list only if the current PR has a base PR at all.
-            prs_for_base_branch = code_hosting_client.get_open_pull_requests_by_head(LocalBranchShortName(pr.base))
+            prs_for_base_branch = self.code_hosting_client.get_open_pull_requests_by_head(LocalBranchShortName(pr.base))
         if style == PRDescriptionIntroStyle.UP_ONLY and len(prs_for_base_branch) == 0:
             return ''
-        spec = code_hosting_client._spec
+        spec = self.code_hosting_spec
         pr_short_name = spec.pr_short_name
-        if all_open_prs_preloaded is not None:
-            all_open_prs = all_open_prs_preloaded
-        else:
-            display_name = spec.display_name
-            determine_what = 'chain' if style == PRDescriptionIntroStyle.UP_ONLY else 'tree'
-            print(f'Checking for open {display_name} {pr_short_name}s '
-                  f'(to determine {pr_short_name} {determine_what})... ', end='', flush=True)
-            all_open_prs = code_hosting_client.get_open_pull_requests()
-            print(fmt('<green><b>OK</b></green>'))
 
-        pr_up_path = list(reversed(self.__get_upwards_path_including_pr(spec, pr, all_open_prs)))
+        pr_up_path = list(reversed(self.__get_upwards_path_including_pr(pr)))
         if style == PRDescriptionIntroStyle.FULL:
-            pr_down_tree = self.__get_downwards_tree_excluding_pr(pr, all_open_prs)
+            pr_down_tree = self.__get_downwards_tree_excluding_pr(pr)
         else:
             pr_down_tree = []
         if len(pr_up_path) == 1 and pr_down_tree == []:
@@ -2817,9 +2799,9 @@ class MacheteClient:
                 opt_push_untracked=True,
                 opt_yes=opt_yes)
 
-        code_hosting_client = spec.create_client(domain=domain, organization=base_org_repo_remote.organization,
-                                                 repository=base_org_repo_remote.repository)
-        current_user: Optional[str] = code_hosting_client.get_current_user_login()
+        self.code_hosting_client = spec.create_client(domain=domain, organization=base_org_repo_remote.organization,
+                                                      repository=base_org_repo_remote.repository)
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
         debug(f'organization is {base_org_repo_remote.organization}, repository is {base_org_repo_remote.repository}')
         debug(f'current {spec.display_name} user is ' + (current_user or '<none>'))
 
@@ -2855,28 +2837,23 @@ class MacheteClient:
         ok_str = '<green><b>OK</b></green>'
         print(f'Creating a {"draft " if opt_draft else ""}{spec.pr_short_name} from {bold(head)} to {bold(base)}... ', end='', flush=True)
 
-        pr: PullRequest = code_hosting_client.create_pull_request(
+        pr: PullRequest = self.code_hosting_client.create_pull_request(
             head=head, head_org_repo=head_org_repo, base=base,
             title=title, description=description, draft=opt_draft)
         print(fmt(f'{ok_str}, see `{pr.html_url}`'))
 
-        style = self.__get_pr_description_into_style_from_config(spec)
+        style = self.__get_pr_description_into_style_from_config()
         # If base branch has NOT originally been found on the remote,
         # we can be sure that a longer chain of PRs above the newly-created PR does NOT exist.
         # So in the default UP_ONLY mode, we can skip generating the intro completely.
-        all_open_prs = None
         if base_branch_found_on_remote or style == PRDescriptionIntroStyle.FULL:
-            if opt_update_related_descriptions:
-                print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True)
-                all_open_prs = code_hosting_client.get_open_pull_requests()
-                print(fmt('<green><b>OK</b></green>'))
             # As the description may include the reference to this PR itself (in case of a chain of >=2 PRs),
             # let's update the PR description after it's already created (so that we know the current PR's number).
-            new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, all_open_prs_preloaded=all_open_prs)
+            new_description = self.__get_updated_pull_request_description(pr)
             if new_description.strip() != description.strip():
                 print(f'Updating description of {pr.display_text()} to include '
                       f'the chain of {spec.pr_short_name}s... ', end='', flush=True)
-                code_hosting_client.set_description_of_pull_request(pr.number, new_description)
+                self.code_hosting_client.set_description_of_pull_request(pr.number, new_description)
                 print(fmt(ok_str))
 
         milestone_path: str = self.__git.get_main_git_subpath('info', 'milestone')
@@ -2886,12 +2863,12 @@ class MacheteClient:
             milestone = None
         if milestone:
             print(f'Setting milestone of {pr.display_text()} to {bold(milestone)}... ', end='', flush=True)
-            code_hosting_client.set_milestone_of_pull_request(pr.number, milestone=milestone)
+            self.code_hosting_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.display_text()}... ', end='', flush=True)
-            code_hosting_client.add_assignees_to_pull_request(pr.number, [current_user])
+            self.code_hosting_client.add_assignees_to_pull_request(pr.number, [current_user])
             print(fmt(ok_str))
 
         reviewers_path = self.__git.get_main_git_subpath('info', 'reviewers')
@@ -2903,7 +2880,7 @@ class MacheteClient:
             print(f'Adding {", ".join(bold(reviewer) for reviewer in reviewers)} '
                   f'as reviewer{"s" if len(reviewers) > 1 else ""} to {pr.display_text()}... ',
                   end='', flush=True)
-            code_hosting_client.add_reviewers_to_pull_request(pr.number, reviewers)
+            self.code_hosting_client.add_reviewers_to_pull_request(pr.number, reviewers)
             print(fmt(ok_str))
 
         self.__annotations[head] = Annotation(self.__pull_request_annotation(spec, pr, current_user))
@@ -2911,7 +2888,7 @@ class MacheteClient:
 
         if opt_update_related_descriptions:
             print(f"Updating descriptions of other {spec.pr_short_name}s...")
-            self.update_pull_request_descriptions(spec, all_open_prs_preloaded=all_open_prs, related=True)
+            self.update_pull_request_descriptions(spec, related=True)
 
     def __handle_diverged_and_newer_state(
             self,


Test patch:
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index a9c17c9..f0829be 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -145,7 +145,7 @@ class TestGitHubCreatePR(BaseTest):
 
             Checking if base branch ignore-trailing exists in origin remote... YES
             Creating a draft PR from chore/fields to ignore-trailing... OK, see www.github.com
-            Checking for open GitHub PRs (to determine PR chain)... OK
+            Checking for open GitHub PRs... OK
             Updating description of PR #5 to include the chain of PRs... OK
             Setting milestone of PR #5 to 42... OK
             Adding github_user as assignee to PR #5... OK
@@ -234,12 +234,12 @@ class TestGitHubCreatePR(BaseTest):
 
             Checking if base branch master exists in origin remote... YES
             Creating a PR from hotfix/add-trigger to master... OK, see www.github.com
-            Checking for open GitHub PRs... OK
             Updating description of PR #6 to include the chain of PRs... OK
             Setting milestone of PR #6 to 42... OK
             Adding github_user as assignee to PR #6... OK
             Adding foo, bar as reviewers to PR #6... OK
             Updating descriptions of other PRs...
+            Checking for open GitHub PRs... OK
             Description of PR #3 (ignore-trailing -> hotfix/add-trigger) has been updated
             Description of PR #5 (chore/fields -> ignore-trailing) has been updated
             """
@@ -623,7 +623,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch branch-1 exists in origin_2 remote... YES
         Creating a PR from feature to branch-1... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #16 to include the chain of PRs... OK
         """  # noqa: E501
 
@@ -686,7 +686,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch feature exists in origin_1 remote... YES
         Creating a PR from feature_1 to feature... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #17 to include the chain of PRs... OK
         """
         assert_success(
@@ -724,7 +724,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch feature exists in origin_1 remote... YES
         Creating a PR from feature_2 to feature... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #18 to include the chain of PRs... OK
         """
         assert_success(
@@ -746,7 +746,7 @@ class TestGitHubCreatePR(BaseTest):
         Added branch feature_3 onto feature_2
         Checking if base branch feature_2 exists in origin_1 remote... YES
         Creating a PR from feature_3 to feature_2... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #19 to include the chain of PRs... OK
         """  # noqa: E501
         assert_success(
@@ -777,7 +777,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch feature_3 exists in origin_1 remote... YES
         Creating a PR from feature_4 to feature_3... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #20 to include the chain of PRs... OK
         """
         assert_success(
@@ -809,7 +809,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch feature_3 exists in origin_1 remote... YES
         Creating a PR from feature_5 to feature_3... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #21 to include the chain of PRs... OK
         """
         assert_success(
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index 169169f..6b87a36 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -253,7 +253,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #20 has been switched to feature\n'
-            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
+            'Checking for open GitHub PRs... OK\n'
             'Description of PR #20 has been updated\n'
         )
         pr20 = github_api_state.get_pull_by_number(20)
@@ -335,7 +335,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #30 has been switched to feature_2\n'
-            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
+            'Checking for open GitHub PRs... OK\n'
             'Description of PR #30 has been updated\n'
         )
         pr30 = github_api_state.get_pull_by_number(30)
@@ -375,7 +375,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #30 has been switched to feature\n'
-            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
+            'Checking for open GitHub PRs... OK\n'
             'Description of PR #30 has been updated\n'
         )
         pr30 = github_api_state.get_pull_by_number(30)
@@ -426,7 +426,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #15 has been switched to branch-1\n'
-            'Checking for open GitHub PRs (to determine PR tree)... OK\n'
+            'Checking for open GitHub PRs... OK\n'
             'Description of PR #15 has been updated\n'
         )
         pr15 = github_api_state.get_pull_by_number(15)
diff --git a/tests/test_gitlab_create_mr.py b/tests/test_gitlab_create_mr.py
index cc811d3..ad93a22 100644
--- a/tests/test_gitlab_create_mr.py
+++ b/tests/test_gitlab_create_mr.py
@@ -144,7 +144,7 @@ class TestGitLabCreateMR(BaseTest):
 
             Checking if target branch ignore-trailing exists in origin remote... YES
             Creating a draft MR from chore/fields to ignore-trailing... OK, see www.gitlab.com
-            Checking for open GitLab MRs (to determine MR chain)... OK
+            Checking for open GitLab MRs... OK
             Updating description of MR !5 to include the chain of MRs... OK
             Setting milestone of MR !5 to 42... OK
             Adding gitlab_user as assignee to MR !5... OK
@@ -232,12 +232,12 @@ class TestGitLabCreateMR(BaseTest):
 
             Checking if target branch master exists in origin remote... YES
             Creating a MR from hotfix/add-trigger to master... OK, see www.gitlab.com
-            Checking for open GitLab MRs... OK
             Updating description of MR !6 to include the chain of MRs... OK
             Setting milestone of MR !6 to 42... OK
             Adding gitlab_user as assignee to MR !6... OK
             Adding foo, bar as reviewers to MR !6... OK
             Updating descriptions of other MRs...
+            Checking for open GitLab MRs... OK
             Description of MR !3 (ignore-trailing -> hotfix/add-trigger) has been updated
             Description of MR !5 (chore/fields -> ignore-trailing) has been updated
         """)
@@ -602,7 +602,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch branch-1 exists in origin_2 remote... YES
         Creating a MR from feature to branch-1... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !16 to include the chain of MRs... OK
         """  # noqa: E501
 
@@ -665,7 +665,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch feature exists in origin_1 remote... YES
         Creating a MR from feature_1 to feature... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !17 to include the chain of MRs... OK
         """
         assert_success(
@@ -703,7 +703,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch feature exists in origin_1 remote... YES
         Creating a MR from feature_2 to feature... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !18 to include the chain of MRs... OK
         """
         assert_success(
@@ -725,7 +725,7 @@ class TestGitLabCreateMR(BaseTest):
         Added branch feature_3 onto feature_2
         Checking if target branch feature_2 exists in origin_1 remote... YES
         Creating a MR from feature_3 to feature_2... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !19 to include the chain of MRs... OK
         """  # noqa: E501
         assert_success(
@@ -756,7 +756,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch feature_3 exists in origin_1 remote... YES
         Creating a MR from feature_4 to feature_3... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !20 to include the chain of MRs... OK
         """
         assert_success(
@@ -788,7 +788,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch feature_3 exists in origin_1 remote... YES
         Creating a MR from feature_5 to feature_3... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !21 to include the chain of MRs... OK
         """
         assert_success(
diff --git a/tests/test_gitlab_retarget_mr.py b/tests/test_gitlab_retarget_mr.py
index 0d218de..22f8800 100644
--- a/tests/test_gitlab_retarget_mr.py
+++ b/tests/test_gitlab_retarget_mr.py
@@ -259,7 +259,7 @@ class TestGitLabRetargetMR(BaseTest):
         assert_success(
             ['gitlab', 'retarget-mr'],
             'Target branch of MR !20 has been switched to feature\n'
-            'Checking for open GitLab MRs (to determine MR chain)... OK\n'
+            'Checking for open GitLab MRs... OK\n'
             'Description of MR !20 has been updated\n'
         )
         mr20 = gitlab_api_state.get_mr_by_number(20)
@@ -310,7 +310,7 @@ class TestGitLabRetargetMR(BaseTest):
         assert_success(
             ['gitlab', 'retarget-mr'],
             'Target branch of MR !25 has been switched to feature\n'
-            'Checking for open GitLab MRs (to determine MR chain)... OK\n'
+            'Checking for open GitLab MRs... OK\n'
             'Description of MR !25 has been updated\n'
         )
         mr25 = gitlab_api_state.get_mr_by_number(25)
@@ -354,7 +354,7 @@ class TestGitLabRetargetMR(BaseTest):
         assert_success(
             ['gitlab', 'retarget-mr'],
             'Target branch of MR !30 has been switched to feature_2\n'
-            'Checking for open GitLab MRs (to determine MR chain)... OK\n'
+            'Checking for open GitLab MRs... OK\n'
             'Description of MR !30 has been updated\n'
         )
         mr30 = gitlab_api_state.get_mr_by_number(30)
@@ -394,7 +394,7 @@ class TestGitLabRetargetMR(BaseTest):
         assert_success(
             ['gitlab', 'retarget-mr'],
             'Target branch of MR !30 has been switched to feature\n'
-            'Checking for open GitLab MRs (to determine MR chain)... OK\n'
+            'Checking for open GitLab MRs... OK\n'
             'Description of MR !30 has been updated\n'
         )
         mr30 = gitlab_api_state.get_mr_by_number(30)