Repository: VirtusLab/git-machete (python)

Issue:
`git machete github checkout-prs` should enable checking out multiple PRs at once
```
git machete github checkout-prs 123 456 789
git machete github checkout-prs --mine
git machete github checkout-prs --by=PawelLipski
git machete github checkout-prs --all
```

This will make entry into the tool for newcomers smoother than with notoriously inaccurate `git machete discover`.

I'd rather NOT include this feature in v3.4.0 to avoid scope creep (rather in v3.5.0). As for v3.4.0, I'd suggest renaming the current subcommand from `checkout-pr` to `checkout-prs` so that a breaking change (subcommand rename) isn't needed later.

Pull request:
Issue #231: Allow to checkout multiple PRs at once
closes #231 

This is an early draft and there are still open points:
- add regression tests for new `github checkout-prs` flags
- update docs, release notes
- update completion scripts
- refactor misleading variable names

Patch:
diff --git a/README.md b/README.md
index dc94910..c8b62eb 100644
--- a/README.md
+++ b/README.md
@@ -160,9 +160,9 @@ Annotate the branches with GitHub PR numbers: <br/>
 git machete github anno-prs
 ```
 
-Check out the PR into local branch, also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally: <br/>
+Check out the given PRs into local branches, also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally as well: <br/>
 ```shell script
-git machete github checkout-prs <PR_number>
+git machete github checkout-prs [--all | --by=<github-login> | --mine | <PR-number-1> ... <PR-number-N>]
 ```
 
 Create the PR, using the upstream (parent) branch from `.git/machete` as the base: <br/>
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 0bd83b7..6532be8 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,10 @@
 # Release notes
 
+
+## New in git-machete 3.8.0
+
+- added: `--all`, `--mine`, `--by` flags and parameter `<PR-number-1> ... <PR-number-N>` to `git machete github checkout-prs`
+
 ## New in git-machete 3.7.2
 
 - fixed: package version retrieval outside of git repository
diff --git a/completion/git-machete.completion.bash b/completion/git-machete.completion.bash
index 92dad3c..d099672 100644
--- a/completion/git-machete.completion.bash
+++ b/completion/git-machete.completion.bash
@@ -45,7 +45,7 @@ _git_machete() {
         delete-unmanaged) __gitcomp "$common_opts $delete_unmanaged_opts" ;;
         discover) __gitcomp "$common_opts $discover_opts" ;;
         fork-point) __gitcomp "$common_opts $fork_point_opts" ;;
-        github)
+        github) # TODO (#393): Add support in zsh and bash completion for options of `git machete github checkout-prs`
           if [[ ${COMP_WORDS[3]} == "create-pr" ]]; then
             __gitcomp "$common_opts $github_create_pr_opts"
           else
diff --git a/completion/git-machete.completion.zsh b/completion/git-machete.completion.zsh
index 9641f15..ece7669 100644
--- a/completion/git-machete.completion.zsh
+++ b/completion/git-machete.completion.zsh
@@ -217,7 +217,7 @@ __git_machete_github_subcommands() {
   local github_subcommands
   github_subcommands=(
     'anno-prs:annotate the branches based on their corresponding GitHub PR numbers and authors'
-    'checkout-prs:check out the given pull request locally'
+    'checkout-prs:check out the given pull requests locally' # TODO (#393): Add support in zsh and bash completion for options of `git machete github checkout-prs`
     'create-pr:create a PR for the current branch, using the upstream (parent) branch as the PR base'
     'retarget-pr:set the base of the current branch PR to upstream (parent) branch'
   )
diff --git a/completion/git-machete.fish b/completion/git-machete.fish
index e609916..0a95d5d 100644
--- a/completion/git-machete.fish
+++ b/completion/git-machete.fish
@@ -77,7 +77,10 @@ complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand
 # git machete github
 complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a github -d 'Create, retarget and manage GitHub PRs while keeping them reflected in git machete'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr retarget-pr"        -f -a anno-prs     -d 'Annotates the branches based on their corresponding GitHub PR numbers and authors'
-complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr retarget-pr"        -x -a checkout-prs -d 'Check out the head branch of the given pull request (specified by number), also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr retarget-pr"        -x -a checkout-prs -d 'Check out the head branch of the given pull requests (specified by number), also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --all" -f -l all        -d 'Checkout all open PRs'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --by" -f -l by        -d 'Checkout someone"s open PRs'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --mine" -f -l mine        -d 'Checkout open PRs for the current user associated with the Github token'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr retarget-pr"        -f -a create-pr    -d 'Creates a PR for the current branch, using the upstream (parent) branch as the PR base'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr; and not __fish_seen_subcommand_from --draft" -f -l draft        -d 'Creates the new PR as a draft'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr retarget-pr"        -f -a retarget-pr  -d 'Sets the base of the current branch\'s PR to upstream (parent) branch, as seen by git machete (see git machete show up)'
diff --git a/docs/source/cli_help/github.rst b/docs/source/cli_help/github.rst
index 6e046cf..e32c83e 100644
--- a/docs/source/cli_help/github.rst
+++ b/docs/source/cli_help/github.rst
@@ -26,16 +26,27 @@ a GitHub API token with ``repo`` scope is required, see https://github.com/setti
   Any existing annotations are overwritten for the branches that have an opened PR; annotations for the other branches remain untouched.
   Equivalent to ``git machete anno --sync-github-prs``.
 
-``checkout-prs``:
+``checkout-prs [--all | --by=<github-login> | --mine | <PR-number-1> ... <PR-number-N>]``:
 
-  Check out the head branch of the given pull request (specified by number),
+  Check out the head branch of the given pull requests (specified by number),
   also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally.
-  Once pull request is checked out locally, annotate local branches with corresponding pull request numbers.
+  Once the specified pull requests are checked out locally, annotate local branches with corresponding pull request numbers.
+  If only one PR is given, then switch the local repository's HEAD to its head branch.
 
-  **Parameters:**
+  **Options:**
+
+    ``--all``    Checkout all open PRs.
+
+    ``--by``    Checkout open PRs authored by the given Github user.
 
-    **<PR-number>**
-      Pull request number to checkout.
+      **Parameters:**
+        ``<github-login>`` Github account name.
+
+    ``--mine``    Checkout open PRs for the current user associated with the Github token.
+
+  **Parameters:**
+    ``<PR-number-1> ... <PR-number-N>``
+      Pull request numbers to checkout.
 
 ``create-pr [--draft]``:
 
@@ -48,7 +59,7 @@ a GitHub API token with ``repo`` scope is required, see https://github.com/setti
 
   **Options:**
 
-    --draft
+    ``--draft``
       Creates the new PR as a draft.
 
 ``retarget-pr``:
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 368b1f2..99d6aec 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.7.2'
+__version__ = '3.8.0'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index fc8766a..7bd6f99 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -221,10 +221,12 @@ def create_cli_parser() -> argparse.ArgumentParser:
         usage=argparse.SUPPRESS,
         add_help=False,
         parents=[common_args_parser])
-    github_parser.add_argument(
-        'subcommand', choices=['anno-prs', 'checkout-prs', 'create-pr', 'retarget-pr'])
-    github_parser.add_argument('pr_no', nargs='?')
+    github_parser.add_argument('subcommand', choices=['anno-prs', 'checkout-prs', 'create-pr', 'retarget-pr'])
+    github_parser.add_argument('pr_no', nargs='*', type=int)
+    github_parser.add_argument('--all', action='store_true')
+    github_parser.add_argument('--by')
     github_parser.add_argument('--draft', action='store_true')
+    github_parser.add_argument('--mine', action='store_true')
 
     go_parser = subparsers.add_parser(
         'go',
@@ -615,25 +617,24 @@ def launch(orig_args: List[str]) -> None:
             github_subcommand = parsed_cli.subcommand
             machete_client.read_definition_file()
 
-            if 'draft' in parsed_cli and github_subcommand not in {'create-pr'}:
-                raise MacheteException(
-                    "'--draft' option is only valid with 'create-pr' subcommand.")
-            if 'pr_no' in parsed_cli and github_subcommand not in {'checkout-prs'}:
-                raise MacheteException(
-                    "'pr_no' argument is only valid with 'checkout-prs' subcommand.")
+            if 'draft' in parsed_cli and github_subcommand != 'create-pr':
+                raise MacheteException("'--draft' option is only valid with 'create-pr' subcommand.")
+            for command in ('all', 'by', 'mine'):
+                if command in parsed_cli and github_subcommand != 'checkout-prs':
+                    raise MacheteException(f"'--{command}' argument is only valid with 'checkout-prs' subcommand.")
+            if 'pr_no' in parsed_cli and github_subcommand != 'checkout-prs':
+                raise MacheteException("'pr_no' option is only valid with 'checkout-prs' subcommand.")
 
             if github_subcommand == "anno-prs":
                 machete_client.sync_annotations_to_github_prs()
             elif github_subcommand == "checkout-prs":
-                if 'pr_no' not in parsed_cli:
+                if len(parsed_cli_as_dict) > 3 or len(parsed_cli_as_dict) == 2:
                     raise MacheteException(
-                        "Argument to `git machete github checkout-prs` cannot be"
-                        " empty; expected PR number.")
-                try:
-                    pr_no: int = int(parsed_cli.pr_no)
-                except ValueError:
-                    raise MacheteException("PR number is not integer value!")
-                machete_client.checkout_github_prs(pr_no)
+                        f"'checkout-prs' subcommand can take only one of following options: {', '.join(['--all', '--by', '--mine', 'pr-no'])}")
+                machete_client.checkout_github_prs(pr_nos=parsed_cli.pr_no if 'pr_no' in parsed_cli else [],
+                                                   all_opened_prs=parsed_cli.all if 'all' in parsed_cli else False,
+                                                   my_opened_prs=parsed_cli.mine if 'mine' in parsed_cli else False,
+                                                   opened_by=parsed_cli.by if 'by' in parsed_cli else None)
             elif github_subcommand == "create-pr":
                 current_branch = git.get_current_branch()
                 machete_client.create_github_pr(
diff --git a/git_machete/client.py b/git_machete/client.py
index 6aa9ecf..c60373a 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -209,7 +209,14 @@ class MacheteClient:
         with open(self._definition_file_path, "w") as file:
             file.write("\n".join(self.render_tree()) + "\n")
 
-    def add(self, *, branch: LocalBranchShortName, opt_onto: Optional[LocalBranchShortName], opt_as_root: bool, opt_yes: bool) -> None:
+    def add(self,
+            *,
+            branch: LocalBranchShortName,
+            opt_onto: Optional[LocalBranchShortName],
+            opt_as_root: bool,
+            opt_yes: bool,
+            verbose: bool = True
+            ) -> None:
         if branch in self.managed_branches:
             raise MacheteException(
                 f"Branch `{branch}` already exists in the tree of branch dependencies")
@@ -225,7 +232,7 @@ class MacheteClient:
                     f"branch `{remote_branch}` exists.\n")
                 msg = common_line + f"Check out `{branch}` locally?" + get_pretty_choices('y', 'N')
                 opt_yes_msg = common_line + f"Checking out `{branch}` locally..."
-                if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes) in ('y', 'yes'):
+                if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes, verbose=verbose) in ('y', 'yes'):
                     self.__git.create_branch(branch, remote_branch.full_name())
                 else:
                     return
@@ -238,7 +245,7 @@ class MacheteClient:
                        f"of {out_of_str})?" + get_pretty_choices('y', 'N'))
                 opt_yes_msg = (f"A local branch `{branch}` does not exist. "
                                f"Creating out of {out_of_str}")
-                if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes) in ('y', 'yes'):
+                if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes, verbose=verbose) in ('y', 'yes'):
                     # If `--onto` hasn't been explicitly specified, let's try to
                     # assess if the current branch would be a good `onto`.
                     if self.__roots and not opt_onto:
@@ -251,7 +258,8 @@ class MacheteClient:
 
         if opt_as_root or not self.__roots:
             self.__roots += [branch]
-            print(fmt(f"Added branch `{branch}` as a new root"))
+            if verbose:
+                print(fmt(f"Added branch `{branch}` as a new root"))
         else:
             if not opt_onto:
                 upstream = self.__infer_upstream(
@@ -270,7 +278,7 @@ class MacheteClient:
                            f"branch `{upstream}`?" + get_pretty_choices('y', 'N'))
                     opt_yes_msg = (f"Adding `{branch}` onto the inferred upstream"
                                    f" (parent) branch `{upstream}`")
-                    if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes) in ('y', 'yes'):
+                    if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes, verbose=verbose) in ('y', 'yes'):
                         opt_onto = upstream
                     else:
                         return
@@ -280,7 +288,8 @@ class MacheteClient:
                 self.__down_branches[opt_onto].append(branch)
             else:
                 self.__down_branches[opt_onto] = [branch]
-            print(fmt(f"Added branch `{branch}` onto `{opt_onto}`"))
+            if verbose:
+                print(fmt(f"Added branch `{branch}` onto `{opt_onto}`"))
 
         self.managed_branches += [branch]
         self.save_definition_file()
@@ -1393,7 +1402,7 @@ class MacheteClient:
         all_prs: List[GitHubPullRequest] = derive_pull_requests(org, repo)
         self.__sync_annotations_to_definition_file(all_prs, current_user)
 
-    def __sync_annotations_to_definition_file(self, prs: List[GitHubPullRequest], current_user: Optional[str] = None) -> None:
+    def __sync_annotations_to_definition_file(self, prs: List[GitHubPullRequest], current_user: Optional[str] = None, verbose: bool = True) -> None:
         for pr in prs:
             if LocalBranchShortName.of(pr.head) in self.managed_branches:
                 debug('sync_annotations_to_definition_file()',
@@ -1407,7 +1416,8 @@ class MacheteClient:
                          f'than in machete file (`{upstream or "<none, is a root>"}`)')
                     anno += f" WRONG PR BASE or MACHETE PARENT? PR has '{pr.base}'"
                 if self.__annotations.get(LocalBranchShortName.of(pr.head)) != anno:
-                    print(fmt(f'Annotating `{pr.head}` as `{anno}`'))
+                    if verbose:
+                        print(fmt(f'Annotating `{pr.head}` as `{anno}`'))
                     self.__annotations[LocalBranchShortName.of(pr.head)] = anno
             else:
                 debug('sync_annotations_to_definition_file()',
@@ -1810,12 +1820,14 @@ class MacheteClient:
             opt_yes_msg: Optional[str],
             opt_yes: bool,
             override_answer: Optional[str] = None,
-            apply_fmt: bool = True
+            apply_fmt: bool = True,
+            verbose: bool = True
     ) -> str:
         if override_answer:
             return override_answer
         if opt_yes and opt_yes_msg:
-            print(fmt(opt_yes_msg) if apply_fmt else opt_yes_msg)
+            if verbose:
+                print(fmt(opt_yes_msg) if apply_fmt else opt_yes_msg)
             return 'y'
         return input(fmt(msg) if apply_fmt else msg).lower()
 
@@ -1847,68 +1859,95 @@ class MacheteClient:
                 f"Forkpoint {forkpoint_sha} is not ancestor of or the tip "
                 f"of the {branch} branch.")
 
-    def checkout_github_prs(self, pr_no: int) -> None:
+    def checkout_github_prs(self,
+                            pr_nos: Optional[List[int]],
+                            *,
+                            all_opened_prs: bool = False,
+                            my_opened_prs: bool = False,
+                            opened_by: str = None,
+                            verbose: bool = False
+                            ) -> None:
         org: str
         repo: str
         remote: str
         remote, (org, repo) = self.__derive_remote_and_github_org_and_repo()
         current_user: Optional[str] = git_machete.github.derive_current_user_login()
+        if not current_user and my_opened_prs:
+            raise MacheteException(
+                "Could not determine current user name, please check your token.")
+        all_open_prs: List[GitHubPullRequest] = derive_pull_requests(org, repo)
+        valid_prs: List[GitHubPullRequest] = self.__get_valid_pull_requests(pr_nos,
+                                                                            all_opened_prs_from_github=all_open_prs,
+                                                                            org=org,
+                                                                            repo=repo,
+                                                                            all=all_opened_prs,
+                                                                            my=my_opened_prs,
+                                                                            by=opened_by,
+                                                                            user=current_user)
+
         debug('checkout_github_pr()', f'organization is {org}, repository is {repo}')
-        print(f"Fetching {remote}...")
+        if verbose:
+            print(f"Fetching {remote}...")
         self.__git.fetch_remote(remote)
 
-        pr = get_pull_request_by_number_or_none(pr_no, org, repo)
-        if not pr:
-            raise MacheteException(f"PR #{pr_no} is not found in repository `{org}/{repo}`")
-        if pr.full_repository_name:
-            if '/'.join([remote, pr.head]) not in self.__git.get_remote_branches():
-                remote_already_added: Optional[str] = self.__get_added_remote_name_or_none(pr.repository_url)
-                if not remote_already_added:
-                    remote_from_pr: str = pr.full_repository_name.split('/')[0]
-                    if remote_from_pr not in self.__git.get_remotes():
-                        self.__git.add_remote(remote_from_pr, pr.repository_url)
-                    remote_to_fetch: str = remote_from_pr
-                else:
-                    remote_to_fetch = remote_already_added
-                if remote != remote_to_fetch:
-                    print(f"Fetching {remote_to_fetch}...")
-                    self.__git.fetch_remote(remote_to_fetch)
-                if '/'.join([remote_to_fetch, pr.head]) not in self.__git.get_remote_branches():
-                    raise MacheteException(f"Could not check out PR #{pr_no} because its head branch `{pr.head}` is already deleted from `{remote_to_fetch}`.")
-        else:
-            warn(f'Pull request #{pr_no} comes from fork and its repository is already deleted. No remote tracking data will be set up for `{pr.head}` branch.')
-            print(fmt(f"Checking out `{pr.head}` locally..."))
-            checkout_pr_refs(self.__git, remote, pr_no, LocalBranchShortName.of(pr.head))
-            self.flush_caches()
-        if pr.state == 'closed':
-            warn(f'Pull request #{pr_no} is already closed.')
-        debug('checkout_github_pr()', f'found {pr}')
-
-        all_open_prs: List[GitHubPullRequest] = derive_pull_requests(org, repo)
-
-        path: List[LocalBranchShortName] = self.__get_path_from_pr_chain(pr, all_open_prs)
-        reversed_path: List[LocalBranchShortName] = path[::-1]  # need to add from root downwards
-        for index, branch in enumerate(reversed_path):
-            if branch not in self.managed_branches:
-                if index == 0:
-                    self.add(
-                        branch=branch,
-                        opt_as_root=True,
-                        opt_onto=None,
-                        opt_yes=True)
-                else:
-                    self.add(
-                        branch=branch,
-                        opt_onto=reversed_path[index - 1],
-                        opt_as_root=False,
-                        opt_yes=True)
+        pr: Optional[GitHubPullRequest] = None
+        checked_out_prs: List[GitHubPullRequest] = []
+        for pr in sorted(valid_prs, key=lambda x: x.number):
+            if pr.full_repository_name:
+                if '/'.join([remote, pr.head]) not in self.__git.get_remote_branches():
+                    remote_already_added: Optional[str] = self.__get_added_remote_name_or_none(pr.repository_url)
+                    if not remote_already_added:
+                        remote_from_pr: str = pr.full_repository_name.split('/')[0]
+                        if remote_from_pr not in self.__git.get_remotes():
+                            self.__git.add_remote(remote_from_pr, pr.repository_url)
+                        remote_to_fetch: str = remote_from_pr
+                    else:
+                        remote_to_fetch = remote_already_added
+                    if remote != remote_to_fetch:
+                        if verbose:
+                            print(f"Fetching {remote_to_fetch}...")
+                        self.__git.fetch_remote(remote_to_fetch)
+                    if '/'.join([remote_to_fetch, pr.head]) not in self.__git.get_remote_branches():
+                        raise MacheteException(f"Could not check out PR #{pr.number} because its head branch `{pr.head}` is already deleted from `{remote_to_fetch}`.")
+            else:
+                warn(f'Pull request #{pr.number} comes from fork and its repository is already deleted. No remote tracking data will be set up for `{pr.head}` branch.')
+                if verbose:
+                    print(fmt(f"Checking out `{pr.head}` locally..."))
+                checkout_pr_refs(self.__git, remote, pr.number, LocalBranchShortName.of(pr.head))
+                self.flush_caches()
+            if pr.state == 'closed':
+                warn(f'Pull request #{pr.number} is already closed.')
+            debug('checkout_github_pr()', f'found {pr}')
+
+            path: List[LocalBranchShortName] = self.__get_path_from_pr_chain(pr, all_open_prs)
+            reversed_path: List[LocalBranchShortName] = path[::-1]  # need to add from root downwards
+            for index, branch in enumerate(reversed_path):
+                if branch not in self.managed_branches:
+                    if index == 0:
+                        self.add(
+                            branch=branch,
+                            opt_as_root=True,
+                            opt_onto=None,
+                            opt_yes=True,
+                            verbose=verbose)
+                    else:
+                        self.add(
+                            branch=branch,
+                            opt_onto=reversed_path[index - 1],
+                            opt_as_root=False,
+                            opt_yes=True,
+                            verbose=verbose)
+                    if pr not in checked_out_prs:
+                        print(fmt(f"Pull request `#{pr.number}` checked out at local branch `{pr.head}`"))
+                        checked_out_prs.append(pr)
 
         debug('checkout_github_pr()',
               'Current GitHub user is ' + (current_user or '<none>'))
-        self.__sync_annotations_to_definition_file(all_open_prs, current_user)
-
-        self.__git.checkout(LocalBranchShortName.of(pr.head))
-        print(fmt(f"Pull request `#{pr.number}` checked out at local branch `{pr.head}`"))
+        self.__sync_annotations_to_definition_file(all_open_prs, current_user, verbose=verbose)
+        if pr and len(valid_prs) == 1:
+            self.__git.checkout(LocalBranchShortName.of(pr.head))
+            if verbose:
+                print(fmt(f"Switched to local branch `{pr.head}`"))
 
     @staticmethod
     def __get_path_from_pr_chain(current_pr: GitHubPullRequest, all_open_prs: List[GitHubPullRequest]) -> List[LocalBranchShortName]:
@@ -1918,6 +1957,52 @@ class MacheteClient:
             current_pr = utils.find_or_none(lambda x: x.head == current_pr.base, all_open_prs)
         return path
 
+    @staticmethod
+    def __get_valid_pull_requests(prs_list: Optional[List[int]],
+                                  all_opened_prs_from_github: List[GitHubPullRequest],
+                                  org: str,
+                                  repo: str,
+                                  all: bool,
+                                  my: bool,
+                                  by: Optional[str],
+                                  user: Optional[str]) -> List[GitHubPullRequest]:
+        result: List[GitHubPullRequest] = []
+        if prs_list:
+            for pr_no in prs_list:
+                _pr: Optional[GitHubPullRequest] = utils.find_or_none(lambda x: x.number == pr_no,
+                                                                      all_opened_prs_from_github)
+                if _pr:
+                    result.append(_pr)
+                else:
+                    pr_from_github = get_pull_request_by_number_or_none(pr_no, org, repo)
+                    if pr_from_github:
+                        result.append(pr_from_github)
+                    else:
+                        if len(prs_list) > 1:
+                            warn(f"PR #{pr_no} is not found in repository `{org}/{repo}`, skipping.")
+                        else:
+                            raise MacheteException(f"PR #{pr_no} is not found in repository `{org}/{repo}`")
+            if not result:
+                raise MacheteException(
+                    f"Given PRs: {', '.join(map(str, prs_list))} are not found in repository `{org}/{repo}`")
+            return result
+        if all:
+            if not all_opened_prs_from_github:
+                raise MacheteException(f"Currently there is not any pull request opened in repository `{org}/{repo}`")
+            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:
+                raise MacheteException(
+                    f"Current user {user} has no open pull request in repository `{org}/{repo}`")
+            return result
+        elif by:
+            result = [pr for pr in all_opened_prs_from_github if pr.user == by]
+            if not result:
+                raise MacheteException(f"User {by} has no open pull request in repository `{org}/{repo}`")
+            return result
+        return []
+
     def __get_added_remote_name_or_none(self, remote_url: str) -> Optional[str]:
         """
         Function to check if remote is added locally by its url,
diff --git a/git_machete/docs.py b/git_machete/docs.py
index 8560aed..22493c4 100644
--- a/git_machete/docs.py
+++ b/git_machete/docs.py
@@ -320,14 +320,22 @@ long_docs: Dict[str, str] = {
           Any existing annotations are overwritten for the branches that have an opened PR; annotations for the other branches remain untouched.
           Equivalent to `git machete anno --sync-github-prs`.
 
-        <b>`checkout-prs <PR-number>`:</b>
+        <b>`checkout-prs [--all | --by=<github-login> | --mine | <PR-number-1> ... <PR-number-N>]`:</b>
 
-          Check out the head branch of the given pull request (specified by number),
+          Check out the head branch of the given pull requests (specified by number),
           also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally.
-          Once pull request is checked out locally, annotate local branches with corresponding pull request numbers.
+          Once the specified pull requests are checked out locally, annotate local branches with corresponding pull request numbers.
+          If only one PR is given, then switch the local repository's HEAD to its head branch.
+
+          <b>Options:</b>
+            <b>--all</b>     Checkout all open PRs.
+            <b>--by</b>      Checkout open PRs authored by the given Github user.
+              <b>Parameters:</b>
+                <b><github-login></b> Github account name.
+            <b>--mine</b>    Checkout open PRs for the current user associated with the Github token.
 
           <b>Parameters:</b>
-            <b><PR-number></b>    Pull request number to checkout.
+            <b><PR-number-1> ... <PR-number-N></b>    Pull request numbers to checkout.
 
         <b>`create-pr [--draft]`:</b>
 


Test patch:
diff --git a/git_machete/tests/functional/test_machete.py b/git_machete/tests/functional/test_machete.py
index 0a155aa..fc4d788 100644
--- a/git_machete/tests/functional/test_machete.py
+++ b/git_machete/tests/functional/test_machete.py
@@ -2216,13 +2216,13 @@ class MacheteTester(unittest.TestCase):
     ])
 
     @mock.patch('git_machete.cli.exit_script', mock_exit_script)
-    # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_checkout_prs` due to `git fetch` executed by `checkout-prs` subcommand.
+    # 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_PATTERNS', FAKE_GITHUB_REMOTE_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('urllib.request.Request', git_api_state_for_test_checkout_prs.new_request())
     @mock.patch('urllib.request.urlopen', MockContextManager)
-    def test_checkout_prs(self) -> None:
+    def test_github_checkout_prs(self) -> None:
         (
             self.repo_sandbox.new_branch("root")
             .commit("initial commit")
@@ -2370,6 +2370,41 @@ class MacheteTester(unittest.TestCase):
             """
         )
 
+        # all PRs
+        self.launch_command('github', 'checkout-prs', '--all')
+        self.assert_command(
+            ["status"],
+            """
+            master
+            |
+            o-hotfix/add-trigger
+              |
+              o-ignore-trailing  PR #3 (github_user)
+                |
+                o-chore/fields
+
+            develop
+            |
+            o-enhance/feature
+            | |
+            | o-bugfix/feature  PR #6 (github_user)
+            |   |
+            |   o-allow-ownership-link  PR #12 (github_user)
+            |     |
+            |     o-restrict_access  PR #17 (github_user)
+            |       |
+            |       o-chore/redundant_checks  PR #18 (github_user)
+            |
+            o-enhance/add_user *  PR #19 (github_user)
+
+            bugfix/add_user
+            |
+            o-testing/add_user  PR #22 (github_user)
+              |
+              o-chore/comments  PR #24 (github_user)
+            """
+        )
+
         # check against wrong pr number
         repo: str
         org: str
@@ -2410,17 +2445,17 @@ class MacheteTester(unittest.TestCase):
          )
         os.chdir(self.repo_sandbox.local_path)
 
-        expected_msg = ("Fetching origin...\n"
-                        "Fetching testing...\n"
-                        "Warn: Pull request #5 is already closed.\n"
-                        "A local branch `bugfix/remove-n-option` does not exist, but a remote branch `testing/bugfix/remove-n-option` exists.\n"
-                        "Checking out `bugfix/remove-n-option` locally...\n"
-                        "Added branch `bugfix/remove-n-option` onto `develop`\n"
+        expected_msg = ("Warn: Pull request #5 is already closed.\n"
                         "Pull request `#5` checked out at local branch `bugfix/remove-n-option`\n")
 
         self.assert_command(['github', 'checkout-prs', '5'], expected_msg, strip_indentation=False)
 
-    git_api_state_for_test_checkout_prs_fresh_repo = MockGithubAPIState([
+        # Check against multiple PRs
+        expected_msg = ''
+
+        self.assert_command(['github', 'checkout-prs', '3', '12'], expected_msg, strip_indentation=False)
+
+    git_api_state_for_test_github_checkout_prs_fresh_repo = MockGithubAPIState([
         {'head': {'ref': 'comments/add_docstrings', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'improve/refactor'}, 'number': '2', 'html_url': 'www.github.com', 'state': 'open'},
         {'head': {'ref': 'restrict_access', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'allow-ownership-link'}, 'number': '17', 'html_url': 'www.github.com', 'state': 'open'},
         {'head': {'ref': 'improve/refactor', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'chore/sync_to_docs'}, 'number': '1', 'html_url': 'www.github.com', 'state': 'open'},
@@ -2428,12 +2463,12 @@ class MacheteTester(unittest.TestCase):
     ])
 
     @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_checkout_prs_freshly_cloned` due to `git fetch` executed by `checkout-prs` subcommand.
+    # 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_PATTERNS', FAKE_GITHUB_REMOTE_PATTERNS)
     @mock.patch('urllib.request.urlopen', MockContextManager)
-    @mock.patch('urllib.request.Request', git_api_state_for_test_checkout_prs_fresh_repo.new_request())
-    def test_checkout_prs_freshly_cloned(self) -> None:
+    @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) -> None:
         (
             self.repo_sandbox.new_branch("root")
             .commit("initial commit")
@@ -2482,17 +2517,7 @@ class MacheteTester(unittest.TestCase):
         )
         os.chdir(self.repo_sandbox.local_path)
         self.rewrite_definition_file("master")
-        expected_msg = ("Fetching origin...\n"
-                        "A local branch `chore/sync_to_docs` does not exist, but a remote branch `origin/chore/sync_to_docs` exists.\n"
-                        "Checking out `chore/sync_to_docs` locally...\n"
-                        "Added branch `chore/sync_to_docs` as a new root\n"
-                        "A local branch `improve/refactor` does not exist, but a remote branch `origin/improve/refactor` exists.\n"
-                        "Checking out `improve/refactor` locally...\n"
-                        "Added branch `improve/refactor` onto `chore/sync_to_docs`\n"
-                        "A local branch `comments/add_docstrings` does not exist, but a remote branch `origin/comments/add_docstrings` exists.\n"
-                        "Checking out `comments/add_docstrings` locally...\nAdded branch `comments/add_docstrings` onto `improve/refactor`\n"
-                        "Annotating `comments/add_docstrings` as `PR #2 (github_user)`\nAnnotating `improve/refactor` as `PR #1 (github_user)`\n"
-                        "Pull request `#2` checked out at local branch `comments/add_docstrings`\n")
+        expected_msg = "Pull request `#2` checked out at local branch `comments/add_docstrings`\n"
         self.assert_command(
             ['github', 'checkout-prs', '2'],
             expected_msg,
@@ -2514,12 +2539,9 @@ class MacheteTester(unittest.TestCase):
 
         # Check against closed pull request
         self.repo_sandbox.execute('git branch -D sphinx_export')
-        expected_msg = ("Fetching origin...\n"
-                        "Warn: Pull request #23 is already closed.\n"
-                        "A local branch `sphinx_export` does not exist, but a remote branch `origin/sphinx_export` exists.\n"
-                        "Checking out `sphinx_export` locally...\n"
-                        "Added branch `sphinx_export` onto `comments/add_docstrings`\n"
+        expected_msg = ("Warn: Pull request #23 is already closed.\n"
                         "Pull request `#23` checked out at local branch `sphinx_export`\n")
+
         self.assert_command(
             ['github', 'checkout-prs', '23'],
             expected_msg,
@@ -2540,7 +2562,7 @@ class MacheteTester(unittest.TestCase):
             """
         )
 
-    git_api_state_for_test_checkout_prs_from_fork_with_deleted_repo = MockGithubAPIState([
+    git_api_state_for_test_github_checkout_prs_from_fork_with_deleted_repo = MockGithubAPIState([
         {'head': {'ref': 'feature/allow_checkout', 'repo': None}, 'user': {'login': 'github_user'}, 'base': {'ref': 'develop'}, 'number': '2', 'html_url': 'www.github.com', 'state': 'closed'},
         {'head': {'ref': 'bugfix/allow_checkout', 'repo': mock_repository_info}, 'user': {'login': 'github_user'},
          'base': {'ref': 'develop'}, 'number': '3', 'html_url': 'www.github.com', 'state': 'open'}
@@ -2548,12 +2570,12 @@ class MacheteTester(unittest.TestCase):
 
     @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
-    # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_checkout_prs_from_fork_with_deleted_repo` due to `git fetch` executed by `checkout-prs` subcommand.
+    # 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('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
     @mock.patch('git_machete.github.GITHUB_REMOTE_PATTERNS', FAKE_GITHUB_REMOTE_PATTERNS)
     @mock.patch('urllib.request.urlopen', MockContextManager)
-    @mock.patch('urllib.request.Request', git_api_state_for_test_checkout_prs_from_fork_with_deleted_repo.new_request())
-    def test_checkout_prs_from_fork_with_deleted_repo(self) -> None:
+    @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:
         (
             self.repo_sandbox.new_branch("root")
             .commit('initial master commit')
@@ -2563,11 +2585,8 @@ class MacheteTester(unittest.TestCase):
             .push()
         )
         self.launch_command('discover')
-        expected_msg = ("Fetching origin...\n"
-                        "Warn: Pull request #2 comes from fork and its repository is already deleted. No remote tracking data will be set up for `feature/allow_checkout` branch.\n"
-                        "Checking out `feature/allow_checkout` locally...\n"
+        expected_msg = ("Warn: Pull request #2 comes from fork and its repository is already deleted. No remote tracking data will be set up for `feature/allow_checkout` branch.\n"
                         "Warn: Pull request #2 is already closed.\n"
-                        "Added branch `feature/allow_checkout` onto `develop`\n"
                         "Pull request `#2` checked out at local branch `feature/allow_checkout`\n")
         self.assert_command(
             ['github', 'checkout-prs', '2'],