Repository: VirtusLab/git-machete
Pull request #486 (2022-04-08 12:37:25)

3.8.0 release


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4497e6a..3267fb9 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -42,6 +42,7 @@ jobs:
             ensure-release-notes-up-to-date.sh
             ensure-shell-scripts-pass-shellcheck.sh
             ensure-version-bumped.sh
+            prohibit-deploy-step-in-circleci.sh
             prohibit-tab-character.sh
             prohibit-trailing-whitespace.sh
 
@@ -112,36 +113,36 @@ jobs:
     steps:
       - checkout
 
-      - deploy:
+      - run:
           name: '[Dry run] Deploy to PyPI'
           # Side effects: does an actual release, but to the test PyPI
           command: bash ci/pypi/ci-deploy.sh --dry-run
 
-      - deploy:
+      - run:
           name: '[Dry run] Update Homebrew tap with local installation test'
           # Prerequisites: artifacts must be released to test PyPI first
           command: bash ci/homebrew-tap/ci-deploy.sh --dry-run
 
-      - deploy:
+      - run:
           name: '[Dry run] Publish image to Docker Hub'
           command: bash ci/docker/ci-deploy.sh --dry-run
 
-      - deploy:
+      - run:
           name: '[Dry run] Build & upload deb package to PPA'
           # Side effects: potentially pushes a regenerated Docker image
           command: bash ci/deb-ppa-upload/ci-deploy.sh --dry-run
 
-      - deploy:
+      - run:
           name: '[Dry run] Build Nix package'
           # Side effects: potentially pushes a regenerated Docker image
           # Other than that, it just builds the package without publishing anything; there's no special dry-run mode
           command: bash ci/nixpkgs-build/ci-run.sh
 
-      - deploy:
+      - run:
           name: '[Dry run] Build snap'
-          command: bash ci/snap/ci-run.sh
+          command: bash ci/snap/ci-deploy.sh --dry-run
 
-      - deploy:
+      - run:
           name: '[Dry run] Build rpm package'
           # Side effects: potentially pushes a regenerated Docker image
           # Other than that, it just builds the package without publishing anything; there's no special dry-run mode
@@ -152,30 +153,34 @@ jobs:
     steps:
       - checkout
 
-      - deploy:
+      - run:
           name: 'Deploy to PyPI'
           command: bash ci/pypi/ci-deploy.sh
 
-      - deploy:
+      - run:
           name: 'Update Homebrew tap'
           # Prerequisites: artifacts must be released to PyPI first
           command: bash ci/homebrew-tap/ci-deploy.sh
 
-      - deploy:
+      - run:
           name: 'Publish image to Docker Hub'
           command: bash ci/docker/ci-deploy.sh
 
-      - deploy:
+      - run:
           name: 'Build & upload deb package to PPA'
           command: bash ci/deb-ppa-upload/ci-deploy.sh
 
-      - deploy:
+      - run:
+          name: 'Publish snap to the stable channel'
+          command: bash ci/snap/ci-deploy.sh
+
+      - run:
           name: 'Build rpm package'
           command: bash ci/rpm/ci-run.sh
 
       # Let's do this at the very end to avoid opening a GH release (and notifying the watchers)
       # in case anything goes wrong in the previous steps.
-      - deploy:
+      - run:
           name: 'Create GitHub release'
           # Prerequisites: RPM package must be in dist/
           command: |
@@ -184,11 +189,9 @@ jobs:
             bash ci/github-releases/ci-deploy.sh
 
       # Note: there are also releases happening outside of our CI:
-      # 1) every time a commit is pushed to `master`, a Snap build for `edge` channel
-      #    is triggered automatically via GitHub webhook on https://build.snapcraft.io/user/VirtusLab/git-machete,
-      # 2) every time a commit is pushed to `develop` or `master`, a build for docs (`latest` or `stable` version, respectively)
+      # 1) every time a commit is pushed to `develop` or `master`, a build for docs (`latest` or `stable` version, respectively)
       #    is triggered automatically via GitHub webhook on https://readthedocs.org/projects/git-machete/builds/,
-      # 3) every time a https://github.com/r-ryantm bot runs on NixOS/nixpkgs repository,
+      # 2) every time a https://github.com/r-ryantm bot runs on NixOS/nixpkgs repository,
       #    Nix git-machete package is updated with the fresh version/package hash according to the data from PyPI.
 
 mandatory_jobs: &mandatory_jobs
diff --git a/.remarkrc.yml b/.remarkrc.yml
index 64f3c86..1a3866f 100644
--- a/.remarkrc.yml
+++ b/.remarkrc.yml
@@ -1,5 +1,8 @@
----
 plugins:
   validate-links:
   lint-no-dead-urls:
-    skipUrlPatterns: ['https://pypistats.org/packages/git-machete']
+    skipUrlPatterns:
+      - https://github.com/r-ryantm
+      - https://img.shields.io
+      - https://pypistats.org/packages/git-machete
+    gotOptions.timeout: 10000 # ms
diff --git a/.remarkrc_master.yml b/.remarkrc_master.yml
index 9a5fb78..91135e8 100644
--- a/.remarkrc_master.yml
+++ b/.remarkrc_master.yml
@@ -1,3 +1,2 @@
----
 plugins:
-  validate-links:
+  validate-links: {}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 97b0f10..efc6a7e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -63,9 +63,9 @@ Output format of any non-plumbing command can change in non-backward-compatible
 ## FAQ's about Pull Requests
 **What is the proper base for pull request?**
 
-Please set the base of pull request to `develop` branch. Current branch protection rules on github only allow to merge `develop` or `hotfix` branches into `master`.
+Please set the base of pull request to `develop` branch. Current branch protection rules on GitHub only allow to merge `develop` or `hotfix` branches into `master`.
 
-**Who closes Github comments? Author of changes, reviewer or initiator of the conversation?**
+**Who closes GitHub comments? Author of changes, reviewer or initiator of the conversation?**
 
 It makes sense to close comment:
 
@@ -75,7 +75,7 @@ It makes sense to close comment:
 **Do you make squash before develop?**
 
 Any technique is okay as long as there are [NO unnecessary merge commits](https://slides.com/plipski/git-machete#/8).
-`Squash and merge` from Github is okay, fast-forward made from console or via `git machete advance` & `git push` is ok too.
+`Squash and merge` from GitHub is okay, fast-forward made from console or via `git machete advance` & `git push` is ok too.
 
 **Is there any commit message convention?**
 
@@ -83,7 +83,7 @@ Nothing special, as long as they look neat in the sense that they are written in
 
 **How do you know that the comment has been approved?**
 
-As in the first point, if the PR author accepts the suggested comment without any additional comments, simply `Resolve` on Github will suffice. There is no need to reply things like "Accepted", "Done", etc as it just spams the reviewer's email.
+As in the first point, if the PR author accepts the suggested comment without any additional comments, simply `Resolve` on GitHub will suffice. There is no need to reply things like "Accepted", "Done", etc as it just spams the reviewer's email.
 
 **Can I resolve all comments in a single commit or each comment in an individual commit?**
 
@@ -105,7 +105,7 @@ Review fixes should be pushed on separate commits for easier viewing on GitHub (
          git merge --no-ff develop
          git push origin master
 
-1. Verify that the release has been created on [Github](https://github.com/VirtusLab/git-machete/releases)
+1. Verify that the release has been created on [GitHub](https://github.com/VirtusLab/git-machete/releases)
    and that a `git-machete-<VERSION>-1.noarch.rpm` file is present under the Assets.
 
 1. Verify that the latest version is uploaded to [PyPI](https://pypi.org/project/git-machete).
@@ -114,18 +114,13 @@ Review fixes should be pushed on separate commits for easier viewing on GitHub (
 
 1. Verify that a Docker image for the new version has been pushed to Docker Hub: [gitmachete/git-machete](https://hub.docker.com/r/gitmachete/git-machete/tags).
 
-1. Verify that a build started for [Snap](https://build.snapcraft.io/user/VirtusLab/git-machete).
-   If not, check `snapcraft.io` webhook on Github (under Settings > Webhooks) &mdash;
-   sometimes the Snap Store's build system responds with 500 status for no clear reason, in such case `Redeliver` the call.
+1. Verify that a newly released version is present in `latest/stable` channel in [Snap](https://snapcraft.io/git-machete/releases).
 
-   Once the build is ready, install the `latest/edge` revision locally (`sudo snap install --edge --classic git-machete`)
+   Install the `stable` revision locally (`sudo snap install --classic git-machete`)
    and verify that it works correctly, esp. if it comes to push/pull via ssh/https and editor (`git machete edit` and interactive rebases).
 
-   Then, perform a release from `latest/edge` to `latest/stable` for both `i386` and `amd64`
-   from [Snapcraft web dashboard](https://snapcraft.io/git-machete/releases) or via CLI.
-
 1. Verify that a build started for [docs at Read the Docs](https://readthedocs.org/projects/git-machete/builds/).
-   If not, check `readthedocs.org` webhook on Github (under Settings > Webhooks).
+   If not, check `readthedocs.org` webhook on GitHub (under Settings > Webhooks).
 
    Once the build is ready, verify the [doc contents](https://git-machete.readthedocs.io/en/stable).
 
diff --git a/README.md b/README.md
index dc94910..157867b 100644
--- a/README.md
+++ b/README.md
@@ -155,14 +155,9 @@ Useful for merging the child branch to the current branch in a linear fashion (w
 
 ### GitHub integration
 
-Annotate the branches with GitHub PR numbers: <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 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/>
-```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/>
@@ -170,9 +165,9 @@ Create the PR, using the upstream (parent) branch from `.git/machete` as the bas
 git machete github create-pr [--draft]
 ```
 
-Sets the base of the current branch's PR to its upstream (parent) branch, as seen by git machete: <br/>
+Synchronize with the remote repository: checkout your open PRs, delete unmanaged branches and also delete untracked managed branches with no downstream branch: <br/>
 ```shell script
-git machete github retarget-pr
+git machete github sync
 ```
 
 **Note**: for private repositories, a GitHub API token with `repo` access is required.
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 0bd83b7..b4bf828 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,13 @@
 # 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`
+- fixed: cherry-pick/merge/rebase/revert is detected on a per-worktree basis
+- added: command `git machete clean` with `--checkout-my-github-prs` flag and its equivalent `git machete github sync`
+- added: `--delete` flag to `git machete slide-out` command for deleting slid-out branches from git
+
 ## New in git-machete 3.7.2
 
 - fixed: package version retrieval outside of git repository
@@ -269,8 +277,8 @@
 
 - fixed: remove stray ANSI escape characters in ASCII-only mode
 - improved: `machete-status-branch` hook now receives `ASCII_ONLY` env var depending on whether `status` runs in ASCII-only mode
-- fixed: artifact upload to Github Releases
-- added: creation and upload of Debian packages to Github Releases
+- fixed: artifact upload to GitHub Releases
+- added: creation and upload of Debian packages to GitHub Releases
 
 ## New in git-machete 2.12.3
 
diff --git a/blogs/docker-ci-tricks-1/blog.md b/blogs/docker-ci-tricks-1/blog.md
index 29a508a..82cfacd 100644
--- a/blogs/docker-ci-tricks-1/blog.md
+++ b/blogs/docker-ci-tricks-1/blog.md
@@ -21,7 +21,7 @@ and [docker-compose](https://docs.docker.com/compose/).
 
 ## High-level overview of the setup
 
-Let's start with the project layout (also available [on Github](https://github.com/VirtusLab/git-machete)):
+Let's start with the project layout (also available [on GitHub](https://github.com/VirtusLab/git-machete)):
 
 ![project layout](project-layout.png)
 
diff --git a/blogs/docker-ci-tricks-2/blog.md b/blogs/docker-ci-tricks-2/blog.md
index a92bafc..67545b5 100644
--- a/blogs/docker-ci-tricks-2/blog.md
+++ b/blogs/docker-ci-tricks-2/blog.md
@@ -259,4 +259,4 @@ More details on the git-machete tool itself can be found in the usage guide:
 [part 1 (basic usage)](https://medium.com/virtuslab/make-your-way-through-the-git-rebase-jungle-with-git-machete-e2ed4dbacd02)
 and [part 2 (advanced features)](https://medium.com/virtuslab/git-machete-strikes-again-traverse-the-git-rebase-jungle-even-faster-with-v2-0-f43ebaf8abb0).
 
-Contributions (and stars on [Github](https://github.com/VirtusLab/git-machete)) are more than welcome!
+Contributions (and stars on [GitHub](https://github.com/VirtusLab/git-machete)) are more than welcome!
diff --git a/ci/checks/ensure-consistent-style-for-fork-point.sh b/ci/checks/ensure-consistent-style-for-fork-point.sh
new file mode 100755
index 0000000..473ff78
--- /dev/null
+++ b/ci/checks/ensure-consistent-style-for-fork-point.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
+source "$self_dir"/utils.sh
+
+if git grep '[fF]orkpoint' -- :^ci/checks/ensure-consistent-style-for-fork-point.sh; then
+  die "Please use 'fork point' or 'Fork point' or 'fork_point' instead of 'forkpoint' or 'Forkpoint'."
+fi
diff --git a/ci/checks/ensure-consistent-style-for-github.sh b/ci/checks/ensure-consistent-style-for-github.sh
new file mode 100755
index 0000000..260dde8
--- /dev/null
+++ b/ci/checks/ensure-consistent-style-for-github.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
+source "$self_dir"/utils.sh
+
+if git grep -we Github -- :^ci/checks/ensure-consistent-style-for-github.sh; then
+  die "Please use 'GitHub' instead of 'Github'."
+fi
diff --git a/ci/checks/ensure-links-correct.sh b/ci/checks/ensure-links-correct.sh
index 2ee8c18..05cbb35 100755
--- a/ci/checks/ensure-links-correct.sh
+++ b/ci/checks/ensure-links-correct.sh
@@ -4,9 +4,9 @@ set -e -o pipefail -u
 
 # Downtime of the linked websites shouldn't block a release.
 if [[ ${CIRCLE_BRANCH-} != master ]]; then
-  extra_options='--rc-path=.remarkrc.yml'
+  rc_path=.remarkrc.yml
 else
-  extra_options='--rc-path=.remarkrc_master.yml'
+  rc_path=.remarkrc_master.yml
 fi
 
-remark $extra_options --ignore-path=.gitignore  --frail .
+remark --frail --ignore-path=.gitignore --rc-path=$rc_path .
diff --git a/ci/checks/ensure-release-notes-up-to-date.sh b/ci/checks/ensure-release-notes-up-to-date.sh
index 976b0a1..ef29b45 100755
--- a/ci/checks/ensure-release-notes-up-to-date.sh
+++ b/ci/checks/ensure-release-notes-up-to-date.sh
@@ -2,7 +2,7 @@
 
 set -e -o pipefail -u
 
-current_version=$(grep '__version__ = ' git_machete/__init__.py | cut -d\' -f2)
+current_version=$(python3 setup.py --version)
 release_notes_version=$(egrep -o '(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)' RELEASE_NOTES.md --max-count 1)
 
 if [[ $current_version != "$release_notes_version" ]]; then
diff --git a/ci/checks/ensure-shell-scripts-pass-shellcheck.sh b/ci/checks/ensure-shell-scripts-pass-shellcheck.sh
index 4ba7d22..7d4196a 100755
--- a/ci/checks/ensure-shell-scripts-pass-shellcheck.sh
+++ b/ci/checks/ensure-shell-scripts-pass-shellcheck.sh
@@ -2,4 +2,4 @@
 
 set -e -o pipefail -u
 
-git ls-files '*.sh' | xargs shellcheck --check-sourced --exclude=SC2090,SC2125 --severity=warning --shell=bash
+git ls-files '*.sh' | xargs shellcheck --check-sourced --exclude=SC2090,SC2125,SC1090 --severity=warning --shell=bash
diff --git a/ci/checks/ensure-version-bumped.sh b/ci/checks/ensure-version-bumped.sh
index b9f0553..dd75425 100755
--- a/ci/checks/ensure-version-bumped.sh
+++ b/ci/checks/ensure-version-bumped.sh
@@ -4,7 +4,7 @@ set -e -o pipefail -u
 
 last_tag=$(git describe --tags --abbrev=0)
 last_tag_version=${last_tag/v/}
-current_version=$(grep '__version__ = ' git_machete/__init__.py | cut -d\' -f2)
+current_version=$(python3 setup.py --version)
 
 if [[ "$(printf "$current_version\n$last_tag_version" | sort --version-sort | tail -n 1)" == "$last_tag_version" ]]; then
   echo "Please increment version with new release! Last released version: $last_tag_version"
diff --git a/ci/checks/prohibit-deploy-step-in-circleci.sh b/ci/checks/prohibit-deploy-step-in-circleci.sh
new file mode 100755
index 0000000..2c914f9
--- /dev/null
+++ b/ci/checks/prohibit-deploy-step-in-circleci.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+if yq --exit-status eval '.jobs.*.steps[] | select(has("deploy"))' .circleci/config.yml 2>/dev/null; then
+  echo
+  echo 'The `deploy` step is deprecated (https://circleci.com/docs/2.0/configuration-reference/#deploy-deprecated) and does NOT mask the values of secret environment variables!'
+  echo 'Use `run` step for the sake of security.'
+  exit 1
+fi
diff --git a/ci/checks/utils.sh b/ci/checks/utils.sh
new file mode 100644
index 0000000..e7360fb
--- /dev/null
+++ b/ci/checks/utils.sh
@@ -0,0 +1,19 @@
+function error() {
+  red='\033[91m'
+  endc='\033[0m'
+
+  if [[ $# -ge 1 ]]; then
+    if [[ -t 1 ]]; then
+      echo -e "${red}>>> $* <<<${endc}"
+    else
+      echo -e ">>> $* <<<"
+    fi
+  fi
+}
+
+function die() {
+  echo
+  error "$@"
+  echo
+  exit 1
+}
diff --git a/ci/deb-ppa-upload/build-context/entrypoint.sh b/ci/deb-ppa-upload/build-context/entrypoint.sh
index 04abc31..1c4e177 100644
--- a/ci/deb-ppa-upload/build-context/entrypoint.sh
+++ b/ci/deb-ppa-upload/build-context/entrypoint.sh
@@ -14,7 +14,7 @@ echo "$SSH_PRIVATE_KEY_CONTENTS_BASE64" | base64 -d > ~/.ssh/id_rsa
 chmod 400 ~/.ssh/id_rsa
 
 set -x
-VERSION=$(cut -d\' -f2 git_machete/__init__.py)
+VERSION=$(python3 setup.py --version)
 export VERSION
 envsubst '$VERSION' < debian/files.envsubst > debian/files
 cp LICENSE debian/copyright
diff --git a/ci/deb-ppa-upload/local-run.sh b/ci/deb-ppa-upload/local-run.sh
index 4b1a06f..d4b4ee0 100755
--- a/ci/deb-ppa-upload/local-run.sh
+++ b/ci/deb-ppa-upload/local-run.sh
@@ -2,11 +2,10 @@
 
 set -e -o pipefail -u
 
-source ../local-run-commons.sh deb-ppa-upload
-check_env gpg-ssh.env
+source "$(git rev-parse --show-toplevel)"/ci/local-run-commons.sh deb-ppa-upload
 export_directory_hash deb-ppa-upload
 cd "$(git rev-parse --show-toplevel)"/ci/deb-ppa-upload/
-
+check_env gpg-ssh.env
 
 set -x
 export TARGET=local
diff --git a/ci/docker/ci-deploy.sh b/ci/docker/ci-deploy.sh
old mode 100644
new mode 100755
index 18772bc..9f343bd
--- a/ci/docker/ci-deploy.sh
+++ b/ci/docker/ci-deploy.sh
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-set -e -o pipefail -u -x
+set -e -o pipefail -u
 
 if [[ ${1-} == "--dry-run" || ${CIRCLE_BRANCH-} != "master" ]]; then
   do_push=false
@@ -8,7 +8,7 @@ else
   do_push=true
 fi
 
-version=$(grep '__version__ = ' git_machete/__init__.py | cut -d\' -f2)
+version=$(python3 setup.py --version)
 
 docker build \
   -t gitmachete/git-machete:$version \
diff --git a/ci/github-releases/ci-deploy.sh b/ci/github-releases/ci-deploy.sh
index 31b6edf..11084d8 100644
--- a/ci/github-releases/ci-deploy.sh
+++ b/ci/github-releases/ci-deploy.sh
@@ -2,7 +2,7 @@
 
 set -e -o pipefail -u -x
 
-tag=v$(grep '__version__ = ' git_machete/__init__.py | cut -d\' -f2)
+tag=v$(python3 setup.py --version)
 # Note that this will also create a git tag on the remote
 # (since apparently all non-draft releases on GitHub must have a corresponding git tag).
 hub release create "$tag" \
diff --git a/ci/homebrew-tap/ci-deploy.sh b/ci/homebrew-tap/ci-deploy.sh
index fc3821f..e95a98d 100644
--- a/ci/homebrew-tap/ci-deploy.sh
+++ b/ci/homebrew-tap/ci-deploy.sh
@@ -10,7 +10,7 @@ else
   pypi_host=pypi.org
 fi
 
-version=$(grep '__version__ = ' git_machete/__init__.py | cut -d\' -f2)
+version=$(python3 setup.py --version)
 
 git clone https://${GITHUB_TOKEN}@github.com/VirtusLab/homebrew-git-machete.git ../homebrew-git-machete
 cd ../homebrew-git-machete/
diff --git a/ci/nixpkgs-build/build-context/entrypoint.sh b/ci/nixpkgs-build/build-context/entrypoint.sh
index 0efcc7c..16dddc3 100755
--- a/ci/nixpkgs-build/build-context/entrypoint.sh
+++ b/ci/nixpkgs-build/build-context/entrypoint.sh
@@ -10,9 +10,6 @@ sed -i -f- $EXPRESSION_PATH <<EOF
   s/version = ".*"/version = "$version"/
   s/rev = \".*\"/rev = \"$GIT_REVISION\"/
   s/sha256 = ".*"/sha256 = "$source_hash"/
-  /git init/d
-  s/stestr run/pytest/
-  s/stestr/pytest/
 EOF
 cat $EXPRESSION_PATH
 
diff --git a/ci/nixpkgs-build/local-run.sh b/ci/nixpkgs-build/local-run.sh
index ddff71c..aeeac05 100755
--- a/ci/nixpkgs-build/local-run.sh
+++ b/ci/nixpkgs-build/local-run.sh
@@ -4,7 +4,7 @@ set -e -o pipefail -u
 
 cd "$(git rev-parse --show-toplevel)"/ci/nixpkgs-build/
 
-source ../local-run-commons.sh nixpkgs-build
+source "$(git rev-parse --show-toplevel)"/ci/local-run-commons.sh nixpkgs-build
 export_directory_hash nixpkgs-build
 
 git_revision=$(git rev-parse "@{upstream}")
diff --git a/ci/snap/README.md b/ci/snap/README.md
index 0217b94..1c88e13 100644
--- a/ci/snap/README.md
+++ b/ci/snap/README.md
@@ -1,3 +1,1 @@
 Note that this directory is distinct from top-level [snap/](https://github.com/VirtusLab/git-machete/tree/master/snap) directory.
-This directory only contains a script that triggers a dry-run of snap builds,
-and is **not** used when actually building the snap.
diff --git a/ci/snap/ci-deploy.sh b/ci/snap/ci-deploy.sh
new file mode 100644
index 0000000..5350834
--- /dev/null
+++ b/ci/snap/ci-deploy.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+sudo apt-get update
+sudo apt-get install -y snapd
+sudo snap install review-tools
+sudo snap install snapcraft --classic
+sudo lxd init --minimal
+# `--use-lxd` applied to use a LXD container instead of a VM, to work around lack of support for KVM on CircleCI VMs.
+snapcraft --use-lxd
+
+if [[ ${1-} == "--dry-run" || ${CIRCLE_BRANCH-} != "master" ]]; then
+  ! command -v git-machete
+  sudo snap install git-machete*.snap --dangerous --classic
+  git machete version
+  sudo snap remove git-machete
+else
+  echo "$SNAPCRAFT_LOGIN_CREDENTIALS_CONTENTS_BASE64" | base64 -d > ~/.snapcraft.login
+  snapcraft login --with ~/.snapcraft.login
+  snapcraft upload --release=stable git-machete*.snap
+  snapcraft status git-machete
+fi
diff --git a/ci/snap/ci-run.sh b/ci/snap/ci-run.sh
deleted file mode 100644
index 42afb03..0000000
--- a/ci/snap/ci-run.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env bash
-
-set -e -o pipefail -u -x
-
-sudo apt-get update
-sudo apt-get install -y snapd
-sudo snap install snapcraft --classic
-sudo lxd init --minimal
-# `--use-lxd` applied to use a LXD container instead of a VM, to work around lack of support for KVM on CircleCI VMs.
-snapcraft --use-lxd
-
-! command -v git-machete
-sudo snap install git-machete_*.snap --dangerous --classic
-git machete version
-sudo snap remove git-machete
diff --git a/ci/snap/export-snapcraft-credentials b/ci/snap/export-snapcraft-credentials
new file mode 100755
index 0000000..dbfdff6
--- /dev/null
+++ b/ci/snap/export-snapcraft-credentials
@@ -0,0 +1,14 @@
+
+if [[ "$OSTYPE" == "darwin"* ]]; then
+  wrap0_flag=""
+else
+  wrap0_flag="--wrap=0"
+fi
+
+snapcraft export-login ~/.snapcraft.login
+
+echo ".snapcraft.login file: \n"
+cat ~/.snapcraft.login
+echo ".snapcraft.login file encoded with base64: \n"
+echo $(base64 $wrap0_flag ~/.snapcraft.login)
+echo "\nThis value is used in as CircleCI's secret as environment variable SNAPCRAFT_LOGIN_CREDENTIALS_CONTENTS_BASE64."
diff --git a/ci/tox/local-run.sh b/ci/tox/local-run.sh
index d70f053..656ab94 100755
--- a/ci/tox/local-run.sh
+++ b/ci/tox/local-run.sh
@@ -2,7 +2,7 @@
 
 set -e -o pipefail -u
 
-source ../local-run-commons.sh tox
+source "$(git rev-parse --show-toplevel)"/ci/local-run-commons.sh tox
 
 export_directory_hash tox
 cd "$(git rev-parse --show-toplevel)"/ci/tox/
diff --git a/completion/git-machete.completion.bash b/completion/git-machete.completion.bash
index 92dad3c..8573372 100644
--- a/completion/git-machete.completion.bash
+++ b/completion/git-machete.completion.bash
@@ -1,12 +1,12 @@
 #!/usr/bin/env bash
 
 _git_machete() {
-  local cmds="add advance anno d delete-unmanaged diff discover e edit file fork-point g github go help is-managed l list log reapply s show slide-out squash status traverse update version"
+  local cmds="add advance anno clean d delete-unmanaged diff discover e edit file fork-point g github go help is-managed l list log reapply s show slide-out squash status traverse update version"
   local help_topics="$cmds format hooks"
 
   local categories="addable managed slidable slidable-after unmanaged with-overridden-fork-point"
   local directions="down first last next prev root up"
-  local github_subcommands="anno-prs checkout-prs create-pr retarget-pr"
+  local github_subcommands="anno-prs checkout-prs create-pr retarget-pr sync"
   local locations="current $directions"
   local opt_color_args="always auto never"
   local opt_return_to_args="here nearest-remaining stay"
@@ -16,13 +16,15 @@ _git_machete() {
   local add_opts="-o --onto= -R --as-root -y --yes"
   local advance_opts="-y --yes"
   local anno_opts="-b --branch= -H --sync-github-prs"
+  local clean_opts="-H --checkout-my-github-prs -y --yes"
   local delete_unmanaged_opts="-y --yes"
   local diff_opts="-s --stat"
   local discover_opts="-C --checked-out-since= -l --list-commits -r --roots= -y --yes"
   local fork_point_opts="--inferred --override-to= --override-to-inferred --override-to-parent --unset-override"
   local github_create_pr_opts="--draft"
+  local github_checkout_prs_opts="--all --by= --mine"
   local reapply_opts="-f --fork-point="
-  local slide_out_opts="-d --down-fork-point= -M --merge -n --no-edit-merge --no-interactive-rebase"
+  local slide_out_opts="-d --down-fork-point= --delete -M --merge -n --no-edit-merge --no-interactive-rebase"
   local squash_opts="-f --fork-point="
   local status_opts="--color= -L --list-commits-with-hashes -l --list-commits --no-detect-squash-merges"
   local traverse_opts="-F --fetch -l --list-commits -M --merge -n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase --no-push --no-push-untracked --push --push-untracked --return-to= --start-from= -w --whole -W -y --yes"
@@ -41,6 +43,7 @@ _git_machete() {
         add) __gitcomp "$common_opts $add_opts" ;;
         advance) __gitcomp "$common_opts $advance_opts" ;;
         anno) __gitcomp "$common_opts $anno_opts" ;;
+        clean) __gitcomp "$common_opts $clean_opts" ;;
         d|diff) __gitcomp "$common_opts $diff_opts" ;;
         delete-unmanaged) __gitcomp "$common_opts $delete_unmanaged_opts" ;;
         discover) __gitcomp "$common_opts $discover_opts" ;;
@@ -48,6 +51,8 @@ _git_machete() {
         github)
           if [[ ${COMP_WORDS[3]} == "create-pr" ]]; then
             __gitcomp "$common_opts $github_create_pr_opts"
+          elif [[ ${COMP_WORDS[3]} == "checkout-prs" ]]; then
+            __gitcomp "$common_opts $github_checkout_prs_opts"
           else
             __gitcomp "$common_opts"
           fi ;;
@@ -98,6 +103,8 @@ _git_machete() {
                   __gitcomp "$github_subcommands"
                 elif [[ ${COMP_WORDS[3]} == "create-pr" ]]; then
                   __gitcomp "$common_opts $github_create_pr_opts"
+                elif [[ ${COMP_WORDS[3]} == "checkout-prs" ]]; then
+                  __gitcomp "$common_opts $github_checkout_prs_opts"
                 else
                   COMPREPLY=('')
                 fi ;;
diff --git a/completion/git-machete.completion.zsh b/completion/git-machete.completion.zsh
index 9641f15..854f842 100644
--- a/completion/git-machete.completion.zsh
+++ b/completion/git-machete.completion.zsh
@@ -23,7 +23,7 @@ _git-machete() {
             '(-y --yes)'{-y,--yes}'[Do not ask for confirmation whether to create the branch or whether to add onto the inferred upstream]' \
           && ret=0
           ;;
-        (advance|delete-unmanaged)
+        (advance)
           _arguments \
             '(-y --yes)'{-y,--yes}'[Do not ask for confirmation]' \
           && ret=0
@@ -34,6 +34,17 @@ _git-machete() {
             '(-H --sync-github-prs)'{-H,--sync-github-prs}'[Annotate with GitHub PR numbers and authors where applicable]' \
           && ret=0
           ;;
+        (clean)
+          _arguments \
+            '(-H --checkout-my-github-prs)'{-H,--checkout-my-github-prs}'[Checkout your open PRs into local branches]' \
+            '(-y --yes)'{-y,--yes}'[Do not ask for confirmation when deleting unmanaged or untracked branches]' \
+          && ret=0
+          ;;
+        (delete-unmanaged)
+          _arguments \
+            '(-y --yes)'{-y,--yes}'[Do not ask for confirmation when deleting unmanaged branches]' \
+          && ret=0
+          ;;
         (d|diff)
           _arguments \
             '1:: :__git_branch_names' \
@@ -65,7 +76,7 @@ _git-machete() {
           _arguments '1:: :__git_machete_directions_go' && ret=0
           ;;
         (github)
-          _arguments '1:: :__git_machete_github_subcommands' && ret=0
+          __git_machete_github_subcommands
           ;;
         (help)
           _arguments '1:: :__git_machete_help_topics' && ret=0
@@ -91,6 +102,7 @@ _git-machete() {
             # TODO (#113): suggest further branches based on the previous specified branch (like in Bash completion script)
             '*:: :__git_machete_list_slidable' \
             '(-d --down-fork-point)'{-d,--down-fork-point=}'[If updating by rebase, specify fork point commit after which the rebased part of history of the downstream branch is meant to start]: :__git_references' \
+            '(--delete)'--delete'[Delete slid-out branches]' \
             '(-M --merge)'{-M,--merge}'[Update by merge rather than by rebase]' \
             '(-n)'-n'[If updating by rebase, equivalent to --no-interactive-rebase. If updating by merge, equivalent to --no-edit-merge]' \
             '(--no-edit-merge)'--no-edit-merge'[If updating by merge, pass --no-edit flag to underlying git merge]' \
@@ -147,6 +159,7 @@ __git_machete_cmds=(
   'add:Add a branch to the tree of branch dependencies'
   'advance:Fast-forward the current branch to match one of its downstreams and subsequently slide out this downstream'
   'anno:Manage custom annotations'
+  'clean:Delete untracked and unmanaged branches and optionally check out open GitHub PRs'
   'delete-unmanaged:Delete local branches that are not present in the definition file'
   {diff,d}':Diff current working directory or a given branch against its fork point'
   'discover:Automatically discover tree of branch dependencies'
@@ -213,15 +226,46 @@ __git_machete_directions_show() {
   _describe -t directions 'direction' directions "$@"
 }
 
-__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'
-    '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'
-  )
-  _describe -t github_subcommands 'subcommand' github_subcommands "$@"
+
+__git_machete_github_subcommands ()
+{
+  local curcontext="$curcontext" state line
+  typeset -A opt_args
+
+  _arguments -C \
+    ':command:->command' \
+    '*::options:->options'
+
+  case $state in
+    (command)
+
+      local -a 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 requests locally'
+        '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'
+        'sync:synchronize with the remote repository: checkout open PRs for the current user associated with the GitHub token, delete unmanaged branches and also delete untracked managed branches with no downstream branch'
+      )
+      _describe -t commands 'subcommand' github_subcommands
+      ;;
+
+    (options)
+      case $line[1] in
+
+        (create-pr)
+          _arguments '(--draft)'--draft'[Creates the new PR as draft]'
+        ;;
+
+        (checkout-prs)
+          _arguments \
+            '(--all)'--all'[Checkout all open PRs.]' \
+            '(--by)'--by'[Checkout open PRs authored by the given GitHub user]' \
+            '(--mine)'--mine'[Checkout open PRs for the current user associated with the GitHub token.]'
+        ;;
+      esac
+    ;;
+  esac
 }
 
 __git_machete_categories() {
diff --git a/completion/git-machete.fish b/completion/git-machete.fish
index e609916..2dd9e1e 100644
--- a/completion/git-machete.fish
+++ b/completion/git-machete.fish
@@ -40,6 +40,12 @@ complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcom
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from anno"                                                           -x -l branch          -s b -a '(__fish_git_local_branches)' -d 'Branch to set the annotation for'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from anno; and not __fish_seen_subcommand_from --sync-github-prs -H" -f -l sync-github-prs -s H                                  -d 'Annotate with GitHub PR numbers and authors where applicable'
 
+# git machete clean
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands"                                                     -f -a clean                          -d 'Delete untracked and unmanaged branches, and optionally check out open GitHub PRs'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from --checkout-my-github-prs -H"   -f -l checkout-my-github-prs -s H    -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 clean; and not __fish_seen_subcommand_from --yes -y"                      -f -l yes -s y                       -d 'Don\'t ask for confirmation'
+
+
 # git machete delete-unmanaged
 complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands"                                           -f -a delete-unmanaged -d 'Delete local branches that are not present in the definition file'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from delete-unmanaged; and not __fish_seen_subcommand_from --yes -y" -f -l yes -s y         -d 'Don\'t ask for confirmation'
@@ -76,11 +82,15 @@ 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"        -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)'
+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 sync"        -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 sync"        -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 not __fish_seen_subcommand_from anno-prs checkout-prs create-pr retarget-pr sync"        -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 not __fish_seen_subcommand_from anno-prs checkout-prs create-pr retarget-pr sync"        -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)'
+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 sync"        -f -a sync         -d 'Synchronize with the remote repository: checkout open PRs for the current user associated with the GitHub token, delete unmanaged branches and also delete untracked managed branches with no downstream branch'
+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 __fish_seen_subcommand_from create-pr; and not __fish_seen_subcommand_from --draft"      -f -l draft        -d 'Creates the new PR as a draft'
 
 # git machete go
 complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a go -d 'Check out the branch relative to the position of the current branch, accepts down/first/last/next/root/prev/up argument'
@@ -133,7 +143,8 @@ complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand
 # git machete slide-out
 complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a slide-out -d 'Slide out the current branch and sync its downstream (child) branches with its upstream (parent) branch via rebase or merge'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from slide-out; and not __fish_seen_subcommand_from --merge -M --down-fork-point -d"                                                        -x -l down-fork-point -s d  -d 'If updating by rebase, specifies the alternative fork point for downstream branches for the operation. git machete fork-point overrides for downstream branches are recommended over use of this option. See also doc for --fork-point option in git machete help reapply and git machete help update. Not allowed if updating by merge'
-complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from slide-out; and not __fish_seen_subcommand_from --merge -M"                                                                             -f -l merge           -s M  -d 'Update the downstream branch by merge rather than by rebase'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from slide-out; and not __fish_seen_subcommand_from --delete"                                                                               -f -l delete                -d 'Delete branches from git'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from slide-out; and not __fish_seen_subcommand_from --merge -M --down-fork-point -d"                                                        -f -l merge           -s M  -d 'Update the downstream branch by merge rather than by rebase'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from slide-out; and not __fish_seen_subcommand_from -n --no-edit-merge --no-interactive-rebase"                                             -f                    -s n  -d 'If updating by rebase, equivalent to --no-interactive-rebase. If updating by merge, equivalent to --no-edit-merge'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from slide-out; and not __fish_seen_subcommand_from -n --no-edit-merge --no-interactive-rebase; and __fish_seen_subcommand_from --merge -M" -f -l no-edit-merge         -d 'If updating by merge, skip opening the editor for merge commit message while doing git merge (i.e. pass --no-edit flag to underlying git merge). Not allowed if updating by rebase'
 complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from slide-out; and not __fish_seen_subcommand_from -n --no-edit-merge --no-interactive-rebase -M --merge"                                  -f -l no-interactive-rebase -d 'If updating by rebase, run git rebase in non-interactive mode (without -i/--interactive flag). Not allowed if updating by merge'
diff --git a/debian/source/options b/debian/source/options
index 0e3cd9a..c3d517b 100644
--- a/debian/source/options
+++ b/debian/source/options
@@ -31,7 +31,6 @@ tar-ignore = .remarkrc.yml
 tar-ignore = .remarkrc_master.yml
 tar-ignore = RELEASE_NOTES.md
 tar-ignore = snap
-tar-ignore = .stestr.conf
 tar-ignore = *.swp
 tar-ignore = test-results
 tar-ignore = tests
diff --git a/docs/source/cli.rst b/docs/source/cli.rst
index f907335..168b7cd 100644
--- a/docs/source/cli.rst
+++ b/docs/source/cli.rst
@@ -12,7 +12,8 @@ git machete commands and help topics:
 * :ref:`add`              -- Add a branch to the tree of branch dependencies.
 * :ref:`advance`          -- Fast-forward merge one of children to the current branch and then slide out this child
 * :ref:`anno`             -- Manage custom annotations
-* :ref:`delete-unmanaged` -- Delete local branches that are not present in the definition file`
+* :ref:`clean`            -- Delete untracked and unmanaged branches and also optionally check out user's open GitHub PRs
+* :ref:`delete-unmanaged` -- Delete local branches that are not present in the definition file
 * :ref:`diff`             -- Diff current working directory or a given branch against its computed fork point
 * :ref:`discover`         -- Automatically discover tree of branch dependencies
 * :ref:`edit`             -- Edit the definition file
@@ -46,6 +47,7 @@ To get help via CLI run:
 .. include:: cli_help/add.rst
 .. include:: cli_help/advance.rst
 .. include:: cli_help/anno.rst
+.. include:: cli_help/clean.rst
 .. include:: cli_help/delete-unmanaged.rst
 .. include:: cli_help/diff.rst
 .. include:: cli_help/discover.rst
diff --git a/docs/source/cli_help/anno.rst b/docs/source/cli_help/anno.rst
index d04ed66..aaa85c4 100644
--- a/docs/source/cli_help/anno.rst
+++ b/docs/source/cli_help/anno.rst
@@ -22,12 +22,7 @@ then clears the annotation for the current branch (or a branch specified with ``
 If invoked with ``-H`` or ``--sync-github-prs``, annotates the branches based on their corresponding GitHub PR numbers and authors.
 Any existing annotations are overwritten for the branches that have an opened PR; annotations for the other branches remain untouched.
 
-To allow GitHub API access for private repositories (and also to perform side-effecting actions like opening a PR, even in case of public repositories),
-a GitHub API token with ``repo`` scope is required, see https://github.com/settings/tokens. This will be resolved from the first of:
-
-    1. ``GITHUB_TOKEN`` env var,
-    2. current auth token from the ``gh`` GitHub CLI,
-    3. current auth token from the ``hub`` GitHub CLI.
+.. include:: github_api_access.rst
 
 In any other case, sets the annotation for the given/current branch to the given <annotation text>.
 If multiple <annotation text>'s are passed to the command, they are concatenated with a single space.
diff --git a/docs/source/cli_help/clean.rst b/docs/source/cli_help/clean.rst
new file mode 100644
index 0000000..c77c182
--- /dev/null
+++ b/docs/source/cli_help/clean.rst
@@ -0,0 +1,29 @@
+.. _clean:
+
+clean
+-----
+**Usage:**
+
+.. code-block:: shell
+
+    git machete clean [-c|--checkout-my-github-prs] [-y|--yes]
+
+Synchronizes with the remote repository:
+    1. if invoked with ``-H`` or ``--checkout-my-github-prs``, checks out open PRs for the current user associated with the GitHub token and also traverses the chain of pull requests upwards, adding branches one by one to git-machete and checks them out locally as well,
+    2. deletes unmanaged branches,
+    3. deletes untracked managed branches that have no downstream branch.
+
+No branch will be deleted unless explicitly confirmed by the user (or unless ``-y/--yes`` option is passed).
+Equivalent of ``git machete github sync`` if invoked with ``-H`` or ``--checkout-my-github-prs``.
+
+.. include:: github_api_access.rst
+
+**Options:**
+
+-c, --checkout-my-github-prs    Checkout your open PRs into local branches.
+-y, --yes                  Don't ask for confirmation when deleting branches from git.
+
+**Environment variables:**
+
+``GITHUB_TOKEN``
+    GitHub API token.
diff --git a/docs/source/cli_help/github.rst b/docs/source/cli_help/github.rst
index 6e046cf..10efed2 100644
--- a/docs/source/cli_help/github.rst
+++ b/docs/source/cli_help/github.rst
@@ -12,13 +12,7 @@ where ``<subcommand>`` is one of: ``anno-prs``, ``checkout-prs``, ``create-pr``,
 
 Creates, checks out and manages GitHub PRs while keeping them reflected in branch definition file.
 
-To allow GitHub API access for private repositories (and also to perform side-effecting actions like opening a PR, even in case of public repositories),
-a GitHub API token with ``repo`` scope is required, see https://github.com/settings/tokens. This will be resolved from the first of:
-
-    1. ``GITHUB_TOKEN`` env var,
-    2. content of the ``.github-token`` file in the home directory (``~``),
-    3. current auth token from the ``gh`` GitHub CLI,
-    4. current auth token from the ``hub`` GitHub CLI.
+.. include:: github_api_access.rst
 
 ``anno-prs``:
 
@@ -26,16 +20,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 numbers or by a flag),
   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 has been checked out, 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.
+
+      **Parameters:**
+        ``<github-login>`` GitHub account name.
 
-    **<PR-number>**
-      Pull request number to checkout.
+    ``--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,13 +53,23 @@ 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``:
 
   Sets the base of the current branch's PR to upstream (parent) branch, as seen by git machete (see ``git machete show up``).
 
+``sync``:
+
+  Synchronizes with the remote repository:
+
+    1. checks out open PRs for the current user associated with the GitHub token and also traverses the chain of pull requests upwards, adding branches one by one to git-machete and checks them out locally as well,
+    2. deletes unmanaged branches,
+    3. deletes untracked managed branches that have no downstream branch.
+
+  Equivalent of ``git machete clean --checkout-my-github-prs``.
+
 **Environment variables (all subcommands):**
 
 ``GITHUB_TOKEN``
diff --git a/docs/source/cli_help/hooks.rst b/docs/source/cli_help/hooks.rst
index d2bcbb0..ed99c23 100644
--- a/docs/source/cli_help/hooks.rst
+++ b/docs/source/cli_help/hooks.rst
@@ -3,6 +3,7 @@
 hooks
 -----
 As with the standard git hooks, git machete looks for its own specific hooks in ``$GIT_DIR/hooks/*`` (or ``$(git config core.hooksPath)/*``, if set).
+All hooks are executed from the top-level folder of the repository (or top-level folder of worktree/submodule, if applicable).
 
 Note: ``hooks`` is not a command as such, just a help topic (there is no ``git machete hooks`` command).
 
diff --git a/docs/source/cli_help/slide-out.rst b/docs/source/cli_help/slide-out.rst
index a301147..6931df6 100644
--- a/docs/source/cli_help/slide-out.rst
+++ b/docs/source/cli_help/slide-out.rst
@@ -6,7 +6,7 @@ slide-out
 
 .. code-block:: shell
 
-    git machete slide-out [-d|--down-fork-point=<down-fork-point-commit>] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase] [<branch> [<branch> [<branch> ...]]]
+    git machete slide-out [-d|--down-fork-point=<down-fork-point-commit>] [--delete] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase] [<branch> [<branch> [<branch> ...]]]
 
 Removes the given branch (or multiple branches) from the branch tree definition.  If no branch has been specified current branch is assumed as the only branch.
 Then synchronizes the downstream (child) branches of the last specified branch on the top of the upstream (parent) branch of the first specified branch.
@@ -48,6 +48,8 @@ Note: This command doesn't delete any branches from git, just removes them from
 
 -d, --down-fork-point=<down-fork-point-commit>    If updating by rebase, specifies the alternative fork point for downstream branches for the operation. ``git machete fork-point`` overrides for downstream branches are recommended over use of this option. See also doc for ``--fork-point`` option in ``git machete help reapply`` and ``git machete help update``. Not allowed if updating by merge.
 
+--delete                                          Delete the slid-out branches.
+
 -M, --merge                                       Update the downstream branch by merge rather than by rebase.
 
 -n                                                If updating by rebase, equivalent to ``--no-interactive-rebase``. If updating by merge, equivalent to ``--no-edit-merge``.
diff --git a/docs/source/conf.py b/docs/source/conf.py
index f9e9c52..4bf76a9 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -46,7 +46,8 @@ exclude_patterns = [
     'cli.rst',
     'completion.rst',
     'description.rst',
-    'learning_materials.rst'
+    'learning_materials.rst',
+    'github_api_access.rst'
 ]
 
 
diff --git a/docs/source/github_api_access.rst b/docs/source/github_api_access.rst
new file mode 100644
index 0000000..efe6208
--- /dev/null
+++ b/docs/source/github_api_access.rst
@@ -0,0 +1,11 @@
+.. _github_api_access:
+
+ .. note::
+
+    To allow GitHub API access for private repositories (and also to perform side-effecting actions like opening a PR, even in case of public repositories),
+    a GitHub API token with ``repo`` scope is required, see https://github.com/settings/tokens. This will be resolved from the first of:
+
+        1. ``GITHUB_TOKEN`` env var,
+        2. content of the ``.github-token`` file in the home directory (``~``),
+        3. current auth token from the ``gh`` GitHub CLI,
+        4. current auth token from the ``hub`` GitHub CLI.
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..e8d082b 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -36,7 +36,7 @@ command_groups: List[Tuple[str, List[str]]] = [
     ("Build, display and modify the tree of branch dependencies",
      ["add", "anno", "discover", "edit", "status"]),
     ("List, check out and delete branches",
-     ["delete-unmanaged", "go", "is-managed", "list", "show"]),
+     ["clean", "delete-unmanaged", "go", "is-managed", "list", "show"]),
     ("Determine changes specific to the given branch",
      ["diff", "fork-point", "log"]),
     ("Update git history in accordance with the tree of branch dependencies",
@@ -156,6 +156,10 @@ def create_cli_parser() -> argparse.ArgumentParser:
     anno_parser.add_argument(
         '-H', '--sync-github-prs', action='store_true', default=argparse.SUPPRESS)
 
+    clean_parser = subparsers.add_parser('clean', usage=argparse.SUPPRESS, add_help=False, parents=[common_args_parser])
+    clean_parser.add_argument('-H', '--checkout-my-github-prs', action='store_true', default=argparse.SUPPRESS)
+    clean_parser.add_argument('-y', '--yes', action='store_true', default=argparse.SUPPRESS)
+
     delete_unmanaged_parser = subparsers.add_parser(
         'delete-unmanaged',
         usage=argparse.SUPPRESS,
@@ -221,10 +225,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', 'sync'])
+    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',
@@ -294,6 +300,7 @@ def create_cli_parser() -> argparse.ArgumentParser:
         parents=[common_args_parser])
     slide_out_parser.add_argument('branches', nargs='*')
     slide_out_parser.add_argument('-d', '--down-fork-point')
+    slide_out_parser.add_argument('--delete', action='store_true')
     slide_out_parser.add_argument('-M', '--merge', action='store_true')
     slide_out_parser.add_argument('-n', action='store_true')
     slide_out_parser.add_argument('--no-edit-merge', action='store_true')
@@ -377,6 +384,8 @@ def update_cli_opts_using_parsed_args(
             cli_opts.opt_checked_out_since = arg
         elif opt == "color":
             cli_opts.opt_color = arg
+        elif opt == "delete":
+            cli_opts.opt_delete = True
         elif opt == "down_fork_point":
             cli_opts.opt_down_fork_point = AnyRevision.of(arg)
         elif opt == "debug":
@@ -529,22 +538,26 @@ def launch(orig_args: List[str]) -> None:
                     f"{machete_client.definition_file_path} is a directory "
                     "rather than a regular file, aborting")
 
+        should_perform_interactive_slide_out = MacheteClient.should_perform_interactive_slide_out(cmd)
         if cmd == "add":
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             branch = get_branch_arg_or_current_branch(cli_opts, git)
             machete_client.add(
                 branch=branch,
                 opt_onto=cli_opts.opt_onto,
                 opt_as_root=cli_opts.opt_as_root,
-                opt_yes=cli_opts.opt_yes)
+                opt_yes=cli_opts.opt_yes,
+                verbose=True,
+                switch_head_if_new_branch=True)
         elif cmd == "advance":
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
             machete_client.expect_in_managed_branches(current_branch)
             machete_client.advance(branch=current_branch, opt_yes=cli_opts.opt_yes)
         elif cmd == "anno":
-            machete_client.read_definition_file(verify_branches=False)
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out,
+                                                verify_branches=False)
             if cli_opts.opt_sync_github_prs:
                 machete_client.sync_annotations_to_github_prs()
             else:
@@ -554,11 +567,18 @@ def launch(orig_args: List[str]) -> None:
                     machete_client.annotate(branch, parsed_cli.annotation_text)
                 else:
                     machete_client.print_annotation(branch)
+        elif cmd == "clean":
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
+            if 'checkout_my_github_prs' in parsed_cli:
+                machete_client.checkout_github_prs(pr_nos=[],
+                                                   my_opened_prs=True)
+            machete_client.delete_unmanaged(opt_yes=cli_opts.opt_yes)
+            machete_client.delete_untracked(opt_yes=cli_opts.opt_yes)
         elif cmd == "delete-unmanaged":
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             machete_client.delete_unmanaged(opt_yes=cli_opts.opt_yes)
         elif cmd in {"diff", alias_by_command["diff"]}:
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             machete_client.diff(branch=parsed_cli.branch, opt_stat=cli_opts.opt_stat)  # passing None if not specified
         elif cmd == "discover":
             # No need to read definition file.
@@ -574,7 +594,7 @@ def launch(orig_args: List[str]) -> None:
             # No need to read definition file.
             print(os.path.abspath(machete_client.definition_file_path))
         elif cmd == "fork-point":
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             branch = get_branch_arg_or_current_branch(cli_opts, git)
             if cli_opts.opt_inferred:
                 print(machete_client.fork_point(
@@ -604,7 +624,7 @@ def launch(orig_args: List[str]) -> None:
                     use_overrides=True,
                     opt_no_detect_squash_merges=cli_opts.opt_no_detect_squash_merges))
         elif cmd in {"go", alias_by_command["go"]}:
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
             dest = machete_client.parse_direction(
@@ -613,27 +633,27 @@ def launch(orig_args: List[str]) -> None:
                 git.checkout(dest)
         elif cmd == "github":
             github_subcommand = parsed_cli.subcommand
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
 
-            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(set(parsed_cli_as_dict.keys()).intersection({'all', 'by', 'mine', 'pr_no'})) != 1:
                     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 must take only one of the 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,
+                                                   fail_on_missing_current_user_for_my_opened_prs=False)
             elif github_subcommand == "create-pr":
                 current_branch = git.get_current_branch()
                 machete_client.create_github_pr(
@@ -644,8 +664,13 @@ def launch(orig_args: List[str]) -> None:
                 current_branch = git.get_current_branch()
                 machete_client.expect_in_managed_branches(current_branch)
                 machete_client.retarget_github_pr(current_branch)
+            elif github_subcommand == "sync":
+                machete_client.checkout_github_prs(pr_nos=[],
+                                                   my_opened_prs=True)
+                machete_client.delete_unmanaged(opt_yes=False)
+                machete_client.delete_untracked(opt_yes=cli_opts.opt_yes)
         elif cmd == "is-managed":
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             branch = get_branch_arg_or_current_branch(cli_opts, git)
             if branch is None or branch not in machete_client.managed_branches:
                 exit_script(1)
@@ -658,7 +683,7 @@ def launch(orig_args: List[str]) -> None:
                 raise MacheteException(
                     f"`git machete list {category}` does not expect extra arguments")
 
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             res = []
             if category == "addable":
                 def strip_remote_name(remote_branch: RemoteBranchShortName) -> LocalBranchShortName:
@@ -689,16 +714,16 @@ def launch(orig_args: List[str]) -> None:
             if res:
                 print("\n".join(res))
         elif cmd in {"log", alias_by_command["log"]}:
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             branch = get_branch_arg_or_current_branch(cli_opts, git)
             machete_client.log(branch)
         elif cmd == "reapply":
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
             if "fork_point" in parsed_cli:
-                machete_client.check_that_forkpoint_is_ancestor_or_equal_to_tip_of_branch(
-                    forkpoint_sha=cli_opts.opt_fork_point, branch=current_branch)
+                machete_client.check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch(
+                    fork_point_sha=cli_opts.opt_fork_point, branch=current_branch)
             git.rebase_onto_ancestor_commit(
                 current_branch,
                 cli_opts.opt_fork_point or machete_client.fork_point(
@@ -715,7 +740,8 @@ def launch(orig_args: List[str]) -> None:
                         '`show current` with a <branch> argument does not make sense')
                 print(branch)
             else:
-                machete_client.read_definition_file(verify_branches=False)
+                machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out,
+                                                    verify_branches=False)
                 print(
                     '\n'.join(machete_client.parse_direction(
                         direction,
@@ -725,22 +751,23 @@ def launch(orig_args: List[str]) -> None:
                     ))
                 )
         elif cmd == "slide-out":
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             branches = parsed_cli_as_dict.get('branches', [git.get_current_branch()])
             machete_client.slide_out(
                 branches_to_slide_out=list(map(LocalBranchShortName.of, branches)),
+                opt_delete=cli_opts.opt_delete,
                 opt_down_fork_point=cli_opts.opt_down_fork_point,
                 opt_merge=cli_opts.opt_merge,
                 opt_no_interactive_rebase=cli_opts.opt_no_interactive_rebase,
                 opt_no_edit_merge=cli_opts.opt_no_edit_merge)
         elif cmd == "squash":
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
             if "fork_point" in parsed_cli:
-                machete_client.check_that_forkpoint_is_ancestor_or_equal_to_tip_of_branch(
-                    forkpoint_sha=cli_opts.opt_fork_point, branch=current_branch)
+                machete_client.check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch(
+                    fork_point_sha=cli_opts.opt_fork_point, branch=current_branch)
             machete_client.squash(
                 current_branch=current_branch,
                 opt_fork_point=cli_opts.opt_fork_point or machete_client.fork_point(
@@ -748,7 +775,7 @@ def launch(orig_args: List[str]) -> None:
                     use_overrides=True,
                     opt_no_detect_squash_merges=cli_opts.opt_no_detect_squash_merges))
         elif cmd in {"status", alias_by_command["status"]}:
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             machete_client.expect_at_least_one_managed_branch()
             machete_client.status(
                 warn_on_yellow_edges=True,
@@ -764,7 +791,7 @@ def launch(orig_args: List[str]) -> None:
                 raise MacheteException(
                     "Invalid argument for `--return-to`. "
                     "Valid arguments: `here|nearest-remaining|stay`.")
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             machete_client.traverse(
                 opt_fetch=cli_opts.opt_fetch,
@@ -779,21 +806,23 @@ def launch(orig_args: List[str]) -> None:
                 opt_start_from=cli_opts.opt_start_from,
                 opt_yes=cli_opts.opt_yes)
         elif cmd == "update":
-            machete_client.read_definition_file()
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             if "fork_point" in parsed_cli:
-                machete_client.check_that_forkpoint_is_ancestor_or_equal_to_tip_of_branch(
-                    forkpoint_sha=cli_opts.opt_fork_point, branch=git.get_current_branch())
+                machete_client.check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch(
+                    fork_point_sha=cli_opts.opt_fork_point, branch=git.get_current_branch())
             machete_client.update(
                 opt_merge=cli_opts.opt_merge,
                 opt_no_edit_merge=cli_opts.opt_no_edit_merge,
                 opt_no_interactive_rebase=cli_opts.opt_no_interactive_rebase,
                 opt_fork_point=cli_opts.opt_fork_point)
 
+    except KeyboardInterrupt:
+        exit_script(3)
     except (argparse.ArgumentError, argparse.ArgumentTypeError) as e:
         print(get_short_general_usage())
         exit_script(2, e)
-    except (MacheteException, KeyboardInterrupt) as e:
+    except MacheteException as e:
         exit_script(1, e)
     except StopInteraction:
         pass
diff --git a/git_machete/client.py b/git_machete/client.py
index 6aa9ecf..32133f8 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -19,7 +19,7 @@ from git_machete.git_operations import (
 from git_machete.github import (
     add_assignees_to_pull_request, add_reviewers_to_pull_request,
     create_pull_request, checkout_pr_refs, derive_pull_request_by_head, derive_pull_requests,
-    get_parsed_github_remote_url, get_pull_request_by_number_or_none, GitHubPullRequest,
+    get_github_token_possible_providers, get_parsed_github_remote_url, get_pull_request_by_number_or_none, GitHubPullRequest,
     is_github_remote_url, set_base_of_pull_request, set_milestone_of_pull_request)
 from git_machete.utils import (
     get_pretty_choices, flat_map, excluding, fmt, tupled, warn, debug, bold,
@@ -36,7 +36,7 @@ class MacheteClient:
 
     def __init__(self, git: GitContext) -> None:
         self.__git: GitContext = git
-        self._definition_file_path: str = self.__git.get_git_subpath("machete")
+        self._definition_file_path: str = self.__git.get_main_git_subpath("machete")
         self._managed_branches: List[LocalBranchShortName] = []
         self._up_branch: Dict[LocalBranchShortName, Optional[LocalBranchShortName]] = {}  # TODO (#110): default dict with None
         self.__down_branches: Dict[LocalBranchShortName, Optional[List[LocalBranchShortName]]] = {}  # TODO (#110): default dict with []
@@ -82,13 +82,12 @@ class MacheteClient:
             f"machete discover` or `git machete edit`, or edit"
             f" {self._definition_file_path} manually.")
 
-    def read_definition_file(self, verify_branches: bool = True) -> None:
+    def read_definition_file(self, perform_interactive_slide_out: bool, verify_branches: bool = True) -> None:
         with open(self._definition_file_path) as file:
             lines: List[str] = [line.rstrip() for line in file.readlines()]
 
         at_depth = {}
         last_depth = -1
-
         hint = "Edit the definition file manually with `git machete edit`"
 
         invalid_branches: List[LocalBranchShortName] = []
@@ -145,18 +144,23 @@ class MacheteClient:
         if not invalid_branches:
             return
 
-        if len(invalid_branches) == 1:
-            ans: str = self.ask_if(
-                f"Skipping `{invalid_branches[0]}` " +
-                "which is not a local branch (perhaps it has been deleted?).\n" +
-                "Slide it out from the definition file?" +
-                get_pretty_choices("y", "e[dit]", "N"), opt_yes_msg=None, opt_yes=False)
+        if perform_interactive_slide_out:
+            if len(invalid_branches) == 1:
+                ans: str = self.ask_if(
+                    f"Skipping `{invalid_branches[0]}` " +
+                    "which is not a local branch (perhaps it has been deleted?).\n" +
+                    "Slide it out from the definition file?" +
+                    get_pretty_choices("y", "e[dit]", "N"), opt_yes_msg=None, opt_yes=False)
+            else:
+                ans = self.ask_if(
+                    f"Skipping {', '.join(f'`{branch}`' for branch in invalid_branches)}"
+                    " which are not local branches (perhaps they have been deleted?).\n"
+                    "Slide them out from the definition file?" + get_pretty_choices("y", "e[dit]", "N"),
+                    opt_yes_msg=None, opt_yes=False)
         else:
-            ans = self.ask_if(
-                f"Skipping {', '.join(f'`{branch}`' for branch in invalid_branches)}"
-                " which are not local branches (perhaps they have been deleted?).\n"
-                "Slide them out from the definition file?" + get_pretty_choices("y", "e[dit]", "N"),
-                opt_yes_msg=None, opt_yes=False)
+            if len(invalid_branches) > 0:
+                print(f"Warning: sliding invalid branches: {', '.join(invalid_branches)} out of the definition file", file=sys.stderr)
+            ans = 'y'
 
         def recursive_slide_out_invalid_branches(branch_: LocalBranchShortName) -> List[LocalBranchShortName]:
             new_down_branches = flat_map(
@@ -209,7 +213,15 @@ 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,
+            switch_head_if_new_branch: bool
+            ) -> None:
         if branch in self.managed_branches:
             raise MacheteException(
                 f"Branch `{branch}` already exists in the tree of branch dependencies")
@@ -225,8 +237,8 @@ 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'):
-                    self.__git.create_branch(branch, remote_branch.full_name())
+                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(), switch_head=switch_head_if_new_branch)
                 else:
                     return
                 # Not dealing with `onto` here. If it hasn't been explicitly
@@ -238,20 +250,21 @@ 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:
                         current_branch = self.__git.get_current_branch_or_none()
                         if current_branch and current_branch in self.managed_branches:
                             opt_onto = current_branch
-                    self.__git.create_branch(branch, out_of)
+                    self.__git.create_branch(branch, out_of, switch_head=switch_head_if_new_branch)
                 else:
                     return
 
         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 +283,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 +293,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()
@@ -404,11 +418,7 @@ class MacheteClient:
                     "choosing this candidate would form a "
                     "cycle in the resulting graph or the candidate is a stale branch"))
             if upstream:
-                debug(
-                    "discover_tree()",
-                    (f"inferred upstream of {branch} is {upstream}, attaching "
-                     f"{branch} as a child of {upstream}\n")
-                )
+                debug(f"inferred upstream of {branch} is {upstream}, attaching {branch} as a child of {upstream}\n")
                 self.up_branch[branch] = upstream
                 root_of[branch] = upstream
                 if upstream in self.__down_branches:
@@ -416,9 +426,7 @@ class MacheteClient:
                 else:
                     self.__down_branches[upstream] = [branch]
             else:
-                debug(
-                    "discover_tree()",
-                    f"inferred no upstream for {branch}, attaching {branch} as a new root\n")
+                debug(f"inferred no upstream for {branch}, attaching {branch} as a new root\n")
                 self.__roots += [branch]
 
         # Let's remove merged branches for which no downstream branch have been found.
@@ -431,12 +439,9 @@ class MacheteClient:
                         target=upstream,
                         opt_no_detect_squash_merges=False
                 ):
-                    debug(
-                        "discover_tree()",
-                        (f"inferred upstream of {branch} is {upstream}, but "
-                         f"{branch} is merged to {upstream}; skipping {branch}"
-                         f" from discovered tree\n")
-                    )
+                    debug(f"inferred upstream of {branch} is {upstream}, but "
+                          f"{branch} is merged to {upstream}; skipping {branch}"
+                          f" from discovered tree\n")
                     merged_branches_to_skip += [branch]
         if merged_branches_to_skip:
             warn(
@@ -478,15 +483,15 @@ class MacheteClient:
             self.save_definition_file()
             self.edit()
 
-    def slide_out(
-            self,
-            *,
-            branches_to_slide_out: List[LocalBranchShortName],
-            opt_down_fork_point: Optional[AnyRevision],
-            opt_merge: bool,
-            opt_no_interactive_rebase: bool,
-            opt_no_edit_merge: bool
-    ) -> None:
+    def slide_out(self,
+                  *,
+                  branches_to_slide_out: List[LocalBranchShortName],
+                  opt_delete: bool,
+                  opt_down_fork_point: Optional[AnyRevision],
+                  opt_merge: bool,
+                  opt_no_interactive_rebase: bool,
+                  opt_no_edit_merge: bool
+                  ) -> None:
         # Verify that all branches exist, are managed, and have an upstream.
         for branch in branches_to_slide_out:
             self.expect_in_managed_branches(branch)
@@ -504,12 +509,11 @@ class MacheteClient:
                     "Last branch to slide out can't have more than one child branch "
                     "if option `--down-fork-point` is passed.")
 
-            self.check_that_forkpoint_is_ancestor_or_equal_to_tip_of_branch(
-                forkpoint_sha=opt_down_fork_point,
+            self.check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch(
+                fork_point_sha=opt_down_fork_point,
                 branch=children_of_the_last_branch_to_slide_out[0])
 
-        # Verify that all "interior" slide-out branches have a single downstream
-        # pointing to the next slide-out
+        # Verify that all "interior" slide-out branches have a single downstream pointing to the next slide-out
         for bu, bd in zip(branches_to_slide_out[:-1], branches_to_slide_out[1:]):
             dbs = self.__down_branches.get(bu)
             if not dbs or len(dbs) == 0:
@@ -532,6 +536,7 @@ class MacheteClient:
         for branch in branches_to_slide_out:
             self.up_branch[branch] = None
             self.__down_branches[branch] = None
+            self.managed_branches.remove(branch)
         self.__down_branches[new_upstream] = [
             branch for branch in self.__down_branches[new_upstream]
             if branch != branches_to_slide_out[0]]
@@ -561,6 +566,9 @@ class MacheteClient:
                     new_downstream,
                     opt_no_interactive_rebase)
 
+        if opt_delete:
+            self._delete_branches(branches_to_delete=branches_to_slide_out, opt_yes=False)
+
     def advance(self, *, branch: LocalBranchShortName, opt_yes: bool) -> None:
         if not self.__down_branches.get(branch):
             raise MacheteException(
@@ -991,15 +999,14 @@ class MacheteClient:
 
             hook_output = ""
             if hook_executable:
-                debug("status()", f"running machete-status-branch hook ({hook_path}) for branch {branch}")
+                debug(f"running machete-status-branch hook ({hook_path}) for branch {branch}")
                 hook_env = dict(os.environ, ASCII_ONLY=str(utils.ascii_only).lower())
                 status_code, stdout, stderr = utils.popen_cmd(hook_path, branch, cwd=self.__git.get_root_dir(), env=hook_env)
                 if status_code == 0:
                     if not stdout.isspace():
                         hook_output = f"  {stdout.rstrip()}"
                 else:
-                    debug("status()",
-                          f"machete-status-branch hook ({hook_path}) for branch {branch} returned {status_code}; stdout: '{stdout}'; stderr: '{stderr}'")
+                    debug("machete-status-branch hook ({hook_path}) for branch {branch} returned {status_code}; stdout: '{stdout}'; stderr: '{stderr}'")
 
             out.write(current + anno + sync_status + hook_output + "\n")
 
@@ -1029,7 +1036,11 @@ class MacheteClient:
             warn(f"{first_part}.\n\n{second_part}.")
 
     def delete_unmanaged(self, *, opt_yes: bool) -> None:
+        print(bold('Checking for unmanaged branches...'))
         branches_to_delete = excluding(self.__git.get_local_branches(), self.managed_branches)
+        self._delete_branches(branches_to_delete=branches_to_delete, opt_yes=opt_yes)
+
+    def _delete_branches(self, branches_to_delete: List[LocalBranchShortName], opt_yes: bool) -> None:
         current_branch = self.__git.get_current_branch_or_none()
         if current_branch and current_branch in branches_to_delete:
             branches_to_delete = excluding(branches_to_delete, [current_branch])
@@ -1077,8 +1088,7 @@ class MacheteClient:
         if self.__is_merged_to_upstream(
                 branch, opt_no_detect_squash_merges=opt_no_detect_squash_merges):
             fp_sha = self.__git.get_commit_sha_by_revision(branch)
-            debug(f"fork_point_and_containing_branch_pairs({branch})",
-                  f"{branch} is merged to {upstream}; skipping inference, using tip of {branch} ({fp_sha}) as fork point")
+            debug(f"{branch} is merged to {upstream}; skipping inference, using tip of {branch} ({fp_sha}) as fork point")
             return fp_sha, []
 
         if use_overrides:
@@ -1090,26 +1100,22 @@ class MacheteClient:
                     # is NOT a descendant of upstream. In this case it's more
                     # reasonable to assume that upstream (and not overridden_fp_sha)
                     # is the fork point.
-                    debug(f"fork_point_and_containing_branch_pairs({branch})",
-                          f"{branch} is descendant of its upstream {upstream}, but overridden fork point commit {overridden_fp_sha} is NOT a descendant of {upstream}; falling back to {upstream} as fork point")
+                    debug(f"{branch} is descendant of its upstream {upstream}, but overridden fork point commit {overridden_fp_sha} is NOT a descendant of {upstream}; falling back to {upstream} as fork point")
                     return self.__git.get_commit_sha_by_revision(upstream), []
                 else:
-                    debug(f"fork_point_and_containing_branch_pairs({branch})",
-                          f"fork point of {branch} is overridden to {overridden_fp_sha}; skipping inference")
+                    debug(f"fork point of {branch} is overridden to {overridden_fp_sha}; skipping inference")
                     return overridden_fp_sha, []
 
         try:
             fp_sha, containing_branch_pairs = next(self.__match_log_to_filtered_reflogs(branch))
         except StopIteration:
             if upstream and self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()):
-                debug(f"fork_point_and_containing_branch_pairs({branch})",
-                      f"cannot find fork point, but {branch} is descendant of its upstream {upstream}; falling back to {upstream} as fork point")
+                debug(f"cannot find fork point, but {branch} is descendant of its upstream {upstream}; falling back to {upstream} as fork point")
                 return self.__git.get_commit_sha_by_revision(upstream), []
             else:
                 raise MacheteException(f"Cannot find fork point for branch `{branch}`")
         else:
-            debug("fork_point_and_containing_branch_pairs({branch})",
-                  f"commit {fp_sha} is the most recent point in history of {branch} to occur on "
+            debug("commit {fp_sha} is the most recent point in history of {branch} to occur on "
                   "filtered reflog of any other branch or its remote counterpart "
                   f"(specifically: {' and '.join(map(utils.get_second, containing_branch_pairs))})")
 
@@ -1119,12 +1125,10 @@ class MacheteClient:
                 # 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"fork_point_and_containing_branch_pairs({branch})",
-                      f"{upstream} is descendant of its upstream {branch}, but inferred fork point commit {fp_sha} is NOT a descendant of {upstream}; falling back to {upstream} as fork point")
+                debug(f"{upstream} is descendant of its upstream {branch}, but inferred fork point commit {fp_sha} is NOT a descendant of {upstream}; falling back to {upstream} as fork point")
                 return self.__git.get_commit_sha_by_revision(upstream), []
             else:
-                debug(f"fork_point_and_containing_branch_pairs({branch})",
-                      f"choosing commit {fp_sha} as fork point")
+                debug(f"choosing commit {fp_sha} as fork point")
                 return fp_sha, containing_branch_pairs
 
     def fork_point(
@@ -1148,8 +1152,8 @@ class MacheteClient:
             format_with_stat=opt_stat)
 
     def log(self, branch: LocalBranchShortName) -> None:
-        forkpoint = self.fork_point(branch, use_overrides=True, opt_no_detect_squash_merges=False)
-        self.__git.display_branch_history_from_forkpoint(branch.full_name(), forkpoint)
+        fork_point = self.fork_point(branch, use_overrides=True, opt_no_detect_squash_merges=False)
+        self.__git.display_branch_history_from_fork_point(branch.full_name(), fork_point)
 
     def down(self, branch: LocalBranchShortName, pick_mode: bool) -> List[LocalBranchShortName]:
         self.expect_in_managed_branches(branch)
@@ -1261,8 +1265,7 @@ class MacheteClient:
                                   new_downstreams: List[LocalBranchShortName]) -> None:
         hook_path = self.__git.get_hook_path("machete-post-slide-out")
         if self.__git.check_hook_executable(hook_path):
-            debug(f"run_post_slide_out_hook({new_upstream}, {slid_out_branch}, {new_downstreams})",
-                  f"running machete-post-slide-out hook ({hook_path})")
+            debug("running machete-post-slide-out hook ({hook_path})")
             new_downstreams_strings: List[str] = [str(db) for db in new_downstreams]
             exit_code = utils.run_cmd(hook_path, new_upstream, slid_out_branch, *new_downstreams_strings,
                                       cwd=self.__git.get_root_dir())
@@ -1334,10 +1337,7 @@ class MacheteClient:
                            gs_ == f"rebase -i (finish): {branch.full_name()} onto {sha_}"
                            )
             if is_excluded:
-                debug(
-                    (f"filtered_reflog({branch.full_name()}) -> is_excluded_reflog_subject({sha_},"
-                     f" <<<{gs_}>>>)"),
-                    "skipping reflog entry")
+                debug("skipping reflog entry")
             return is_excluded
 
         branch_reflog = self.__git.get_reflog(branch.full_name())
@@ -1347,14 +1347,12 @@ class MacheteClient:
         earliest_sha, earliest_gs = branch_reflog[-1]  # Note that the reflog is returned from latest to earliest entries.
         shas_to_exclude = set()
         if earliest_gs.startswith("branch: Created from"):
-            debug(f"filtered_reflog({branch.full_name()})",
-                  f"skipping any reflog entry with the hash equal to the hash of the earliest (branch creation) entry: {earliest_sha}")
+            debug(f"skipping any reflog entry with the hash equal to the hash of the earliest (branch creation) entry: {earliest_sha}")
             shas_to_exclude.add(earliest_sha)
 
         result = [sha for (sha, gs) in branch_reflog if
                   sha not in shas_to_exclude and not is_excluded_reflog_subject(sha, gs)]
-        debug(f"filtered_reflog({branch.full_name()})",
-              "computed filtered reflog (= reflog without branch creation "
+        debug("computed filtered reflog (= reflog without branch creation "
               "and branch reset events irrelevant for fork point/upstream inference): %s\n" % (", ".join(result) or "<empty>"))
         return result
 
@@ -1387,17 +1385,15 @@ class MacheteClient:
                     f'Multiple non-origin remotes correspond to GitHub in this repository: '
                     f'{", ".join(org_name_for_github_remote.keys())}, aborting')
         current_user: Optional[str] = git_machete.github.derive_current_user_login()
-        debug('sync_annotations_to_github_prs()',
-              'Current GitHub user is ' + (current_user or '<none>'))
+        debug('Current GitHub user is ' + (current_user or '<none>'))
         pr: GitHubPullRequest
         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()',
-                      f'{pr} corresponds to a managed branch')
+                debug(f'{pr} corresponds to a managed branch')
                 anno: str = f'PR #{pr.number}'
                 if pr.user != current_user:
                     anno += f' ({pr.user})'
@@ -1407,11 +1403,11 @@ 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()',
-                      f'{pr} does NOT correspond to a managed branch')
+                debug(f'{pr} does NOT correspond to a managed branch')
         self.save_definition_file()
 
     # Parse and evaluate direction against current branch for show/go commands
@@ -1474,8 +1470,7 @@ class MacheteClient:
                     joined_branch_pairs = ", ".join(map(tupled(branch_pair_to_str), branch_pairs_))
                     yield dim(f"{sha_} => {joined_branch_pairs}")
 
-            debug(f"match_log_to_filtered_reflogs({branch})",
-                  "branches containing the given SHA in their filtered reflog: \n%s\n" % "\n".join(log_result()))
+            debug("branches containing the given SHA in their filtered reflog: \n%s\n" % "\n".join(log_result()))
 
         branch_full_hash: FullCommitHash = self.__git.get_commit_sha_by_revision(branch)
 
@@ -1491,32 +1486,26 @@ class MacheteClient:
 
                 containing_branch_pairs = sorted(filter(tupled(lb_is_not_b), branch_pairs), key=get_second)
                 if containing_branch_pairs:
-                    debug(f"match_log_to_filtered_reflogs({branch})",
-                          f"commit {sha} found in filtered reflog of {' and '.join(map(get_second, branch_pairs))}")
+                    debug(f"commit {sha} found in filtered reflog of {' and '.join(map(get_second, branch_pairs))}")
                     yield sha, containing_branch_pairs
                 else:
-                    debug(f"match_log_to_filtered_reflogs({branch})",
-                          f"commit {sha} found only in filtered reflog of {' and '.join(map(get_second, branch_pairs))}; ignoring")
+                    debug(f"commit {sha} found only in filtered reflog of {' and '.join(map(get_second, branch_pairs))}; ignoring")
             else:
-                debug(f"match_log_to_filtered_reflogs({branch})",
-                      f"commit {sha} not found in any filtered reflog")
+                debug(f"commit {sha} not found in any filtered reflog")
 
     def __infer_upstream(self, branch: LocalBranchShortName, condition: Callable[[LocalBranchShortName], bool] = lambda upstream: True, reject_reason_message: str = "") -> Optional[LocalBranchShortName]:
         for sha, containing_branch_pairs in self.__match_log_to_filtered_reflogs(branch):
-            debug(f"infer_upstream({branch})",
-                  f"commit {sha} found in filtered reflog of {' and '.join(map(get_second, containing_branch_pairs))}")
+            debug(f"commit {sha} found in filtered reflog of {' and '.join(map(get_second, containing_branch_pairs))}")
 
             for candidate, original_matched_branch in containing_branch_pairs:
                 if candidate != original_matched_branch:
-                    debug(f"infer_upstream({branch})",
-                          f"upstream candidate is {candidate}, which is the local counterpart of {original_matched_branch}")
+                    debug(f"upstream candidate is {candidate}, which is the local counterpart of {original_matched_branch}")
 
                 if condition(candidate):
-                    debug(f"infer_upstream({branch})", f"upstream candidate {candidate} accepted")
+                    debug(f"upstream candidate {candidate} accepted")
                     return candidate
                 else:
-                    debug(f"infer_upstream({branch})",
-                          f"upstream candidate {candidate} rejected ({reject_reason_message})")
+                    debug(f"upstream candidate {candidate} rejected ({reject_reason_message})")
         return None
 
     @staticmethod
@@ -1583,8 +1572,7 @@ class MacheteClient:
                 "Consider running:\n",
                 f"  `git machete fork-point --unset-override {branch}`\n"))
             return None
-        debug(f"get_overridden_fork_point({branch})",
-              f"since branch {branch} is descendant of while_descendant_of={while_descendant_of}, fork point of {branch} is overridden to {to}")
+        debug(f"since branch {branch} is descendant of while_descendant_of={while_descendant_of}, fork point of {branch} is overridden to {to}")
         return to
 
     def unset_fork_point_override(self, branch: LocalBranchShortName) -> None:
@@ -1810,12 +1798,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()
 
@@ -1838,77 +1828,112 @@ class MacheteClient:
         self.__branch_pairs_by_sha_in_reflog = None
         self.__git.flush_caches()
 
-    def check_that_forkpoint_is_ancestor_or_equal_to_tip_of_branch(
-            self, forkpoint_sha: AnyRevision, branch: AnyBranchName) -> None:
+    def check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch(
+            self, fork_point_sha: AnyRevision, branch: AnyBranchName) -> None:
         if not self.__git.is_ancestor_or_equal(
-                earlier_revision=forkpoint_sha.full_name(),
+                earlier_revision=fork_point_sha.full_name(),
                 later_revision=branch.full_name()):
             raise MacheteException(
-                f"Forkpoint {forkpoint_sha} is not ancestor of or the tip "
+                f"Fork point {fork_point_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,
+                            fail_on_missing_current_user_for_my_opened_prs: bool = True
+                            ) -> None:
+        print(bold('Checking for open GitHub PRs...'))
         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()
-        debug('checkout_github_pr()', f'organization is {org}, repository is {repo}')
-        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}')
-
+        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.")
+            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(org, repo)
+        applicable_prs: List[GitHubPullRequest] = self.__get_applicable_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(f'organization is {org}, repository is {repo}')
+        if verbose:
+            print(f"Fetching {remote}...")
+        self.__git.fetch_remote(remote)
 
-        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)
-
-        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}`"))
+        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():
+                    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(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,
+                            switch_head_if_new_branch=False)
+                    else:
+                        self.add(
+                            branch=branch,
+                            opt_onto=reversed_path[index - 1],
+                            opt_as_root=False,
+                            opt_yes=True,
+                            verbose=verbose,
+                            switch_head_if_new_branch=False)
+                    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('Current GitHub user is ' + (current_user or '<none>'))
+        self.__sync_annotations_to_definition_file(all_open_prs, current_user, verbose=verbose)
+        if pr and len(checked_out_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 +1943,51 @@ class MacheteClient:
             current_pr = utils.find_or_none(lambda x: x.head == current_pr.base, all_open_prs)
         return path
 
+    @staticmethod
+    def __get_applicable_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:
+                        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:
+                warn(f"Currently there are no pull requests opened in repository `{org}/{repo}`")
+                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 `{user}` has no open pull request in repository `{org}/{repo}`")
+                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 `{by}` has no open pull request in repository `{org}/{repo}`")
+                return []
+            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,
@@ -1939,12 +2009,12 @@ class MacheteClient:
         repo: str
         _, (org, repo) = self.__derive_remote_and_github_org_and_repo()
 
-        debug(f'retarget_github_pr({head})', f'organization is {org}, repository is {repo}')
+        debug(f'organization is {org}, repository is {repo}')
 
         pr: Optional[GitHubPullRequest] = derive_pull_request_by_head(org, repo, head)
         if not pr:
             raise MacheteException(f'No PR is opened in `{org}/{repo}` for branch `{head}`')
-        debug(f'retarget_github_pr({head})', f'found {pr}')
+        debug(f'found {pr}')
 
         new_base: Optional[LocalBranchShortName] = self.up_branch.get(LocalBranchShortName.of(head))
         if not new_base:
@@ -2011,14 +2081,14 @@ class MacheteClient:
                 opt_yes=False)
 
         current_user: Optional[str] = git_machete.github.derive_current_user_login()
-        debug(f'create_github_pr({head})', f'organization is {org}, repository is {repo}')
-        debug(f'create_github_pr({head})', 'current GitHub user is ' + (current_user or '<none>'))
+        debug('organization is {org}, repository is {repo}')
+        debug('current GitHub user is ' + (current_user or '<none>'))
 
         fork_point = self.fork_point(head, use_overrides=True, opt_no_detect_squash_merges=False)
         if not fork_point:
             raise MacheteException(f"Could not find a fork-point for branch {head}.")
         commits: List[GitLogEntry] = self.__git.get_commits_between(fork_point, head)
-        description_path = self.__git.get_git_subpath('info', 'description')
+        description_path = self.__git.get_main_git_subpath('info', 'description')
         description: str = utils.slurp_file_or_empty(description_path)
 
         ok_str = ' <green><b>OK</b></green>'
@@ -2027,7 +2097,7 @@ class MacheteClient:
                                                     description=description, draft=opt_draft)
         print(fmt(f'{ok_str}, see <b>{pr.html_url}</b>'))
 
-        milestone_path: str = self.__git.get_git_subpath('info', 'milestone')
+        milestone_path: str = self.__git.get_main_git_subpath('info', 'milestone')
         milestone: str = utils.slurp_file_or_empty(milestone_path).strip()
         if milestone:
             print(fmt(f'Setting milestone of PR #{pr.number} to {milestone}...'), end='', flush=True)
@@ -2039,7 +2109,7 @@ class MacheteClient:
             add_assignees_to_pull_request(org, repo, pr.number, [current_user])
             print(fmt(ok_str))
 
-        reviewers_path = self.__git.get_git_subpath('info', 'reviewers')
+        reviewers_path = self.__git.get_main_git_subpath('info', 'reviewers')
         reviewers: List[str] = utils.get_non_empty_lines(utils.slurp_file_or_empty(reviewers_path))
         if reviewers:
             print(
@@ -2049,7 +2119,7 @@ class MacheteClient:
                 add_reviewers_to_pull_request(org, repo, 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_git_subpath('info', 'reviewers')} file.\n"
+                    warn(f"There are some invalid reviewers in {self.__git.get_main_git_subpath('info', 'reviewers')} file.\n"
                          "Skipping adding reviewers to pull request.")
                 else:
                     raise e
@@ -2199,7 +2269,12 @@ class MacheteClient:
 
         current_branch = self.__git.get_current_branch()
         if current_branch not in self.managed_branches:
-            self.add(branch=current_branch, opt_onto=opt_onto, opt_as_root=False, opt_yes=opt_yes)
+            self.add(branch=current_branch,
+                     opt_onto=opt_onto,
+                     opt_as_root=False,
+                     opt_yes=opt_yes,
+                     verbose=True,
+                     switch_head_if_new_branch=True)
             if current_branch not in self.managed_branches:
                 raise MacheteException(
                     "Command `github create-pr` can NOT be executed on the branch"
@@ -2277,3 +2352,23 @@ class MacheteClient:
             if ans in ('y', 'yes'):
                 return
             raise MacheteException('Pull request creation interrupted.')
+
+    def delete_untracked(self, opt_yes: bool) -> None:
+        print(bold('Checking for untracked managed branches with no downstream...'))
+        branches_to_delete: List[LocalBranchShortName] = []
+        # TODO (#453): Consider switching to immutable collections for keeping the state (managed_branches etc.).
+        for managed_branch in self.managed_branches.copy():
+            status, _ = self.__git.get_strict_remote_sync_status(managed_branch)
+            if status == SyncToRemoteStatuses.UNTRACKED:
+                if not self.__down_branches.get(managed_branch):
+                    branches_to_delete.append(managed_branch)
+                    self.managed_branches.remove(managed_branch)
+                    self.up_branch[managed_branch] = None
+
+        self._delete_branches(branches_to_delete=branches_to_delete, opt_yes=opt_yes)
+        self.save_definition_file()
+
+    @staticmethod
+    def should_perform_interactive_slide_out(cmd: str) -> bool:
+        interactive_slide_out_safe_commands = {'traverse', 'status'}
+        return sys.stdout.isatty() and cmd in interactive_slide_out_safe_commands
diff --git a/git_machete/docs.py b/git_machete/docs.py
index 8560aed..7167dfa 100644
--- a/git_machete/docs.py
+++ b/git_machete/docs.py
@@ -5,6 +5,7 @@ short_docs: Dict[str, str] = {
     "add": "Add a branch to the tree of branch dependencies",
     "advance": "Fast-forward merge one of children to the current branch and then slide out this child",
     "anno": "Manage custom annotations",
+    "clean": "Delete untracked and unmanaged branches and also optionally check out user's open GitHub PRs",
     "delete-unmanaged": "Delete local branches that are not present in the definition file",
     "diff": "Diff current working directory or a given branch against its computed fork point",
     "discover": "Automatically discover tree of branch dependencies",
@@ -128,8 +129,9 @@ long_docs: Dict[str, str] = {
         To allow GitHub API access for private repositories (and also to perform side-effecting actions like opening a PR, even in case of public repositories),
         a GitHub API token with `repo` scope is required, see `https://github.com/settings/tokens`. This will be resolved from the first of:
         1. `GITHUB_TOKEN` env var,
-        2. current auth token from the `gh` GitHub CLI,
-        3. current auth token from the `hub` GitHub CLI.
+        2. content of the ``.github-token`` file in the home directory (``~``),
+        3. current auth token from the `gh` GitHub CLI,
+        4. current auth token from the `hub` GitHub CLI.
 
         In any other case, sets the annotation for the given/current branch to the given argument.
         If multiple arguments are passed to the command, they are concatenated with a single space.
@@ -140,6 +142,30 @@ long_docs: Dict[str, str] = {
           <b>-b, --branch=<branch></b>      Branch to set the annotation for.
           <b>-H, --sync-github-prs</b>      Annotate with GitHub PR numbers and authors where applicable.
     """,
+    "clean": """
+        <b>Usage:
+          git machete clean [-c|--checkout-my-github-prs] [-y|--yes]
+
+        Synchronizes with the remote repository:
+            1. if invoked with ``-H`` or ``--checkout-my-github-prs``, checks out open PRs for the current user associated with the GitHub token and also traverses the chain of pull requests upwards, adding branches one by one to git-machete and checks them out locally as well,
+            2. deletes unmanaged branches,
+            3. deletes untracked managed branches that have no downstream branch.
+
+        No branch will be deleted unless explicitly confirmed by the user (or unless ``-y/--yes`` option is passed).
+        Equivalent of ``git machete github sync`` if invoked with ``-H`` or ``--checkout-my-github-prs``.
+
+        To allow GitHub API access for private repositories (and also to perform side-effecting actions like opening a PR, even in case of public repositories),
+        a GitHub API token with ``repo`` scope is required, see https://github.com/settings/tokens. This will be resolved from the first of:
+
+            1. ``GITHUB_TOKEN`` env var,
+            2. content of the ``.github-token`` file in the home directory (``~``),
+            3. current auth token from the ``gh`` GitHub CLI,
+            4. current auth token from the ``hub`` GitHub CLI.
+
+        **Options:**
+          <b>--c, --checkout-my-github-prs</b>     Checkout your open PRs into local branches.
+          <b>-y, --yes</b>                         Don't ask for confirmation when deleting branches from git.
+    """,
     "delete-unmanaged": """
         <b>Usage: git machete delete-unmanaged [-y|--yes]</b>
 
@@ -303,7 +329,7 @@ long_docs: Dict[str, str] = {
     """,
     "github": """
         <b>Usage: git machete github <subcommand></b>
-        where <subcommand> is one of: `anno-prs`, `checkout-prs`, `create-pr`, `retarget-pr`.
+        where <subcommand> is one of: `anno-prs`, `checkout-prs`, `create-pr`, `retarget-pr`, `sync`.
 
         Creates, checks out and manages GitHub PRs while keeping them reflected in branch definition file.
 
@@ -320,14 +346,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 numbers or by a flag),
           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 has been checked out, 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>
 
@@ -344,6 +378,14 @@ long_docs: Dict[str, str] = {
         <b>`retarget-pr`:</b>
 
           Sets the base of the current branch's PR to upstream (parent) branch, as seen by git machete (see `git machete show up`).
+
+        <b>`sync`:</b>
+
+            Synchronizes with the remote repository:
+                1. checks out open PRs for the current user associated with the GitHub token and also traverses the chain of pull requests upwards, adding branches one by one to git-machete and checks them out locally as well,
+                2. deletes unmanaged branches,
+                3. deletes untracked managed branches that have no downstream branch.
+          Equivalent of ``git machete clean --checkout-my-github-prs``.
     """,
     "go": """
         <b>Usage: git machete g[o] <direction></b>
@@ -367,6 +409,7 @@ long_docs: Dict[str, str] = {
     """,
     "hooks": """
         As with the standard git hooks, git-machete looks for its own specific hooks in `$GIT_DIR/hooks/*` (or `$(git config core.hooksPath)/*`, if set).
+        All hooks are executed from the top-level folder of the repository (or top-level folder of worktree/submodule, if applicable).
 
         Note: `hooks` is not a command as such, just a help topic (there is no `git machete hooks` command).
 
@@ -489,7 +532,7 @@ long_docs: Dict[str, str] = {
         * `up`:      the direct parent/upstream branch of the given branch.
     """,
     "slide-out": """
-        <b>Usage: git machete slide-out [-d|--down-fork-point=<down-fork-point-commit>] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase] [<branch> [<branch> [<branch> ...]]]</b>
+        <b>Usage: git machete slide-out [-d|--down-fork-point=<down-fork-point-commit>] [--delete] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase] [<branch> [<branch> [<branch> ...]]]</b>
 
         Removes the given branch (or multiple branches) from the branch tree definition. If no branch has been specified current branch is assumed as the only branch.
         Then synchronizes the downstream (child) branches of the last specified branch on the top of the upstream (parent) branch of the first specified branch.
@@ -528,6 +571,8 @@ long_docs: Dict[str, str] = {
                                                             See also doc for `--fork-point` option in `git machete help reapply` and `git machete help update`.
                                                             Not allowed if updating by merge.
 
+          <b>--delete</b>                                          Delete slid-out branches from git.
+
           <b>-M, --merge</b>                                       Update the downstream branch by merge rather than by rebase.
 
           <b>-n</b>                                                If updating by rebase, equivalent to `--no-interactive-rebase`. If updating by merge, equivalent to `--no-edit-merge`.
diff --git a/git_machete/exceptions.py b/git_machete/exceptions.py
index ae09ea3..c9e404d 100644
--- a/git_machete/exceptions.py
+++ b/git_machete/exceptions.py
@@ -15,7 +15,7 @@ class StopInteraction(Exception):
 
 
 class UnprocessableEntityHTTPError(MacheteException):
-    """This exception is raised when Github API returns HTTP status code 422 - Unprocessable Entity.
+    """This exception is raised when GitHub API returns HTTP status code 422 - Unprocessable Entity.
     Such a situation occurs when trying to do something not allowed by GitHub,
     e.g. assigning someone from outside organization as a reviewer
     or creating a pull request for a branch that already has a PR.
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 4080e2c..69e4d1e 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -173,7 +173,8 @@ class GitContext:
     def __init__(self) -> None:
         self._git_version: Optional[Tuple[int, ...]] = None
         self._root_dir: Optional[str] = None
-        self._git_dir: Optional[str] = None
+        self._main_git_dir: Optional[str] = None
+        self._worktree_git_dir: Optional[str] = None
         self.__fetch_done_for: Set[str] = set()
         self.__config_cached: Optional[Dict[str, str]] = None
         self.__remotes_cached: Optional[List[str]] = None
@@ -227,17 +228,17 @@ class GitContext:
         for name, fun in proposed_editor_funs:
             editor = fun()
             if not editor:
-                debug("get_default_editor()", f"'{name}' is undefined")
+                debug(f"'{name}' is undefined")
             else:
                 editor_repr = f"'{name}'{(' (' + editor + ')') if editor != name else ''}"
                 if not utils.find_executable(editor):
-                    debug("get_default_editor()", f"{editor_repr} is not available")
+                    debug(f"{editor_repr} is not available")
                     if name == "$" + git_machete_editor_var:
                         # In this specific case, when GIT_MACHETE_EDITOR is defined but doesn't point to a valid executable,
                         # it's more reasonable/less confusing to raise an error and exit without opening anything.
                         raise MacheteException(f"<b>{editor_repr}</b> is not available")
                 else:
-                    debug("get_default_editor()", f"{editor_repr} is available")
+                    debug(f"{editor_repr} is available")
                     if name != "$" + git_machete_editor_var and self.get_config_attr_or_none('advice.macheteEditorSelection') != 'false':
                         sample_alternative = 'nano' if editor.startswith('vi') else 'vi'
                         print(fmt(f"Opening <b>{editor_repr}</b>.\n",
@@ -265,23 +266,34 @@ class GitContext:
                 raise MacheteException("Not a git repository")
         return self._root_dir
 
-    def __get_git_dir(self) -> str:
-        if not self._git_dir:
+    def __get_worktree_git_dir(self) -> str:
+        if not self._worktree_git_dir:
+            try:
+                self._worktree_git_dir = self._popen_git("rev-parse", "--git-dir").strip()
+            except MacheteException:
+                raise MacheteException("Not a git repository")
+        return self._worktree_git_dir
+
+    def __get_main_git_dir(self) -> str:
+        if not self._main_git_dir:
             try:
                 git_dir: str = self._popen_git("rev-parse", "--git-dir").strip()
                 git_dir_parts = Path(git_dir).parts
                 if len(git_dir_parts) >= 3 and git_dir_parts[-3] == '.git' and git_dir_parts[-2] == 'worktrees':
-                    self._git_dir = os.path.join(*git_dir_parts[:-2])
-                    debug('__get_git_dir', f'git dir pointing to {git_dir} - we are in a worktree; '
-                                           f'using {self._git_dir} as the effective git dir instead')
+                    self._main_git_dir = os.path.join(*git_dir_parts[:-2])
+                    debug(f'git dir pointing to {git_dir} - we are in a worktree; '
+                          f'using {self._main_git_dir} as the effective git dir instead')
                 else:
-                    self._git_dir = git_dir
+                    self._main_git_dir = git_dir
             except MacheteException:
                 raise MacheteException("Not a git repository")
-        return self._git_dir
+        return self._main_git_dir
 
-    def get_git_subpath(self, *fragments: str) -> str:
-        return os.path.join(self.__get_git_dir(), *fragments)
+    def get_worktree_git_subpath(self, *fragments: str) -> str:
+        return os.path.join(self.__get_worktree_git_dir(), *fragments)
+
+    def get_main_git_subpath(self, *fragments: str) -> str:
+        return os.path.join(self.__get_main_git_dir(), *fragments)
 
     def get_git_timespec_parsed_to_unix_timestamp(self, date: str) -> int:
         try:
@@ -441,18 +453,21 @@ class GitContext:
         # Since many people don't use '--set-upstream' flag of 'push' or 'branch', we try to infer the remote if the tracking data is missing.
         return self.get_strict_counterpart_for_fetching_of_branch(branch) or self.__get_inferred_counterpart_for_fetching_of_branch(branch)
 
+    # Note that rebase/cherry-pick/merge/revert all happen on per-worktree basis,
+    # so we need to check .git/worktrees/<worktree>/<file> rather than .git/<file>
+
     def is_am_in_progress(self) -> bool:
         # As of git 2.24.1, this is how 'cmd_rebase()' in builtin/rebase.c checks whether am is in progress.
-        return os.path.isfile(self.get_git_subpath("rebase-apply", "applying"))
+        return os.path.isfile(self.get_worktree_git_subpath("rebase-apply", "applying"))
 
     def is_cherry_pick_in_progress(self) -> bool:
-        return os.path.isfile(self.get_git_subpath("CHERRY_PICK_HEAD"))
+        return os.path.isfile(self.get_worktree_git_subpath("CHERRY_PICK_HEAD"))
 
     def is_merge_in_progress(self) -> bool:
-        return os.path.isfile(self.get_git_subpath("MERGE_HEAD"))
+        return os.path.isfile(self.get_worktree_git_subpath("MERGE_HEAD"))
 
     def is_revert_in_progress(self) -> bool:
-        return os.path.isfile(self.get_git_subpath("REVERT_HEAD"))
+        return os.path.isfile(self.get_worktree_git_subpath("REVERT_HEAD"))
 
     def checkout(self, branch: LocalBranchShortName) -> None:
         self._run_git("checkout", "--quiet", branch, "--")
@@ -565,8 +580,10 @@ class GitContext:
                                                          ))
             return self.__reflogs_cached[branch]
 
-    def create_branch(self, branch: LocalBranchShortName, out_of_revision: AnyRevision) -> None:
-        self._run_git("checkout", "-b", branch, out_of_revision)
+    def create_branch(self, branch: LocalBranchShortName, out_of_revision: AnyRevision, switch_head: bool) -> None:
+        self._run_git("branch", branch, out_of_revision)
+        if switch_head:
+            self._run_git("checkout", branch)
         self.flush_caches()  # the repository state has changed because of a successful branch creation, let's defensively flush all the caches
 
     def flush_caches(self) -> None:
@@ -597,12 +614,12 @@ class GitContext:
 
         # .git/rebase-merge directory exists during cherry-pick-powered rebases,
         # e.g. all interactive ones and the ones where '--strategy=' or '--keep-empty' option has been passed
-        rebase_merge_head_name_file = self.get_git_subpath("rebase-merge", "head-name")
+        rebase_merge_head_name_file = self.get_worktree_git_subpath("rebase-merge", "head-name")
         if os.path.isfile(rebase_merge_head_name_file):
             head_name_file = rebase_merge_head_name_file
 
         # .git/rebase-apply directory exists during the remaining, i.e. am-powered rebases, but also during am sessions.
-        rebase_apply_head_name_file = self.get_git_subpath("rebase-apply", "head-name")
+        rebase_apply_head_name_file = self.get_worktree_git_subpath("rebase-apply", "head-name")
         # Most likely .git/rebase-apply/head-name can't exist during am sessions, but it's better to be safe.
         if not self.is_am_in_progress() and os.path.isfile(rebase_apply_head_name_file):
             head_name_file = rebase_apply_head_name_file
@@ -698,10 +715,7 @@ class GitContext:
         if (earlier_commit_sha, later_commit_sha) in self.__contains_equivalent_tree_cached:
             return self.__contains_equivalent_tree_cached[earlier_commit_sha, later_commit_sha]
 
-        debug(
-            "contains_equivalent_tree",
-            f"earlier_revision={earlier_revision} later_revision={later_revision}",
-        )
+        debug()
 
         earlier_tree_sha = self.get_tree_sha_by_commit_sha(earlier_commit_sha)
 
@@ -733,14 +747,20 @@ class GitContext:
         return matching_remote_branches[0] if len(matching_remote_branches) == 1 else None
 
     def get_merged_local_branches(self) -> List[LocalBranchShortName]:
-        return list(map(
-            lambda branch: LocalBranchFullName.of(branch).to_short_name(),
-            utils.get_non_empty_lines(
-                self._popen_git("for-each-ref", "--format=%(refname)", "--merged", "HEAD", "refs/heads"))
-        ))
+        if self.get_git_version() >= (2, 7, 6):  # earliest version of git to support 'for-each-ref --merged'
+            return list(
+                map(lambda branch: LocalBranchFullName.of(branch).to_short_name(),
+                    utils.get_non_empty_lines(
+                        self._popen_git("for-each-ref", "--format=%(refname)", "--merged", "HEAD", "refs/heads"))))
+        else:
+            return list(
+                filter(lambda branch: self.is_ancestor_or_equal(branch, AnyRevision.of('HEAD')),
+                       map(lambda branch: LocalBranchFullName.of(branch).to_short_name(),
+                           utils.get_non_empty_lines(
+                               self._popen_git("for-each-ref", "--format=%(refname)", "refs/heads")))))
 
     def get_hook_path(self, hook_name: str) -> str:
-        hook_dir: str = self.get_config_attr_or_none("core.hooksPath") or self.get_git_subpath("hooks")
+        hook_dir: str = self.get_config_attr_or_none("core.hooksPath") or self.get_main_git_subpath("hooks")
         return os.path.join(hook_dir, hook_name)
 
     def check_hook_executable(self, hook_path: str) -> bool:
@@ -789,7 +809,7 @@ class GitContext:
 
                 # No need to fix <git-dir>/rebase-apply/author-script,
                 # only <git-dir>/rebase-merge/author-script (i.e. interactive rebases, for the most part) is affected.
-                author_script = self.get_git_subpath("rebase-merge", "author-script")
+                author_script = self.get_worktree_git_subpath("rebase-merge", "author-script")
                 if os.path.isfile(author_script):
                     faulty_line_regex = re.compile("[A-Z0-9_]+='[^']*")
 
@@ -807,7 +827,7 @@ class GitContext:
 
         hook_path = self.get_hook_path("machete-pre-rebase")
         if self.check_hook_executable(hook_path):
-            debug(f"rebase({onto}, {fork_revision}, {branch})", f"running machete-pre-rebase hook ({hook_path})")
+            debug(f"running machete-pre-rebase hook ({hook_path})")
             exit_code = utils.run_cmd(hook_path, onto, fork_revision, branch, cwd=self.get_root_dir())
             if exit_code == 0:
                 do_rebase()
@@ -890,7 +910,7 @@ class GitContext:
             params.append(commit)
         return self._popen_git(*params)
 
-    def display_branch_history_from_forkpoint(self, branch: LocalBranchFullName, fork_point: FullCommitHash) -> int:
+    def display_branch_history_from_fork_point(self, branch: LocalBranchFullName, fork_point: FullCommitHash) -> int:
         return self._run_git("log", f"^{fork_point}", branch)
 
     def commit_tree_with_given_parent_and_message_and_env(
diff --git a/git_machete/github.py b/git_machete/github.py
index 8b246d4..7fd818d 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -151,8 +151,7 @@ def __fire_github_api_request(method: str, path: str, token: Optional[str], requ
     url = host + 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'__fire_github_api_request({method}, {path}, ..., ...)',
-          f'firing a {method} request to {url} with {"a" if token else "no"} bearer token and request body {json_body or "<none>"}')
+    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:
@@ -166,16 +165,17 @@ def __fire_github_api_request(method: str, path: str, token: Optional[str], requ
         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 + f'Make sure that the token provided in `gh auth status` or `~/.config/hub`'
-                                                    f' or <b>{GITHUB_TOKEN_ENV_VAR}</b> is valid and allows for access to `{method.upper()}` https://{host}{path}`.')
+                raise MacheteException(first_line + f'Make sure that the GitHub API token provided by one of the: '
+                                                    f'{get_github_token_possible_providers()}is valid and allows for access to '
+                                                    f'`{method.upper()}` `https://{host}{path}`.')
             else:
                 raise MacheteException(
                     first_line + f'You might not have the required permissions for this repository. '
-                                 f'Provide a GitHub API token with `repo` access via <b>{GITHUB_TOKEN_ENV_VAR}</b> env var or `gh` or `hub`.\n'
+                                 f'Provide a GitHub API token with `repo` access via one of the: {get_github_token_possible_providers()}'
                                  'Visit `https://github.com/settings/tokens` to generate a new one.')
         elif err.code == http.HTTPStatus.NOT_FOUND:
             raise MacheteException(
-                f'Given endpoint: {url}, not found in Github or you don\'t have'
+                f'Given endpoint: {url}, not found in GitHub or you don\'t have'
                 f' the permission to access it (expired token?).')  # TODO (#164): make dedicated exception here
         else:
             first_line = fmt(f'GitHub API returned {err.code} HTTP status with error message: `{err.reason}`\n')
@@ -275,3 +275,10 @@ def get_pull_request_by_number_or_none(number: int, org: str, repo: str) -> Opti
 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_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')
diff --git a/git_machete/options.py b/git_machete/options.py
index e8afde8..e8f854f 100644
--- a/git_machete/options.py
+++ b/git_machete/options.py
@@ -12,6 +12,7 @@ class CommandLineOptions:
         self.opt_checked_out_since: Optional[str] = None
         self.opt_color: str = "auto"
         self.opt_debug: bool = False
+        self.opt_delete: bool = False
         self.opt_down_fork_point: Optional[AnyRevision] = None
         self.opt_draft: bool = False
         self.opt_fetch: bool = False
diff --git a/git_machete/utils.py b/git_machete/utils.py
index c136d47..4505285 100644
--- a/git_machete/utils.py
+++ b/git_machete/utils.py
@@ -1,4 +1,4 @@
-
+import inspect
 from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, TypeVar
 
 import os
@@ -87,7 +87,7 @@ def find_executable(executable: str) -> Optional[str]:
     for p in paths:
         f = os.path.join(p, executable)
         if os.path.isfile(f) and is_executable(f):
-            debug(f"find_executable({executable})", f"found {executable} at {f}")
+            debug(f"found {executable} at {f}")
             return f
     return None
 
@@ -154,9 +154,15 @@ def get_pretty_choices(*choices: str) -> str:
     return f" ({', '.join(map_truthy_only(format_choice, choices))}) "
 
 
-def debug(hdr: str, msg: str) -> None:
+def debug(msg: Optional[str] = None) -> None:
     if debug_mode:
-        print(f"{bold(hdr)}: {dim(msg)}", file=sys.stderr)
+        function_name = bold(inspect.stack()[1].function)
+        args, _, _, values = inspect.getargvalues(inspect.stack()[1].frame)
+        function_args = bold(f"({', '.join([arg+'='+str(values[arg]) if arg != 'self' else 'self' for arg in args])})")
+        if msg is None:
+            print(f"{function_name}{function_args}", file=sys.stderr)
+        else:
+            print(f"{function_name}{function_args}: {dim(msg)}", file=sys.stderr)
 
 
 def run_cmd(cmd: str, *args: str, **kwargs: Any) -> int:
@@ -195,8 +201,7 @@ def chdir_upwards_until_current_directory_exists() -> None:
                 # it doesn't propagate to the parent process (which is typically a shell).
                 os.chdir(os.path.pardir)
                 current_directory = get_current_directory_or_none()
-            debug("chdir_upwards_until_current_directory_exists()",
-                  f"current directory did not exist, chdired up into {current_directory}")
+            debug(f"current directory did not exist, chdired up into {current_directory}")
         current_directory_confirmed_to_exist = True
 
 
diff --git a/hook_samples/machete-post-slide-out b/hook_samples/machete-post-slide-out
index e703a56..d35318e 100755
--- a/hook_samples/machete-post-slide-out
+++ b/hook_samples/machete-post-slide-out
@@ -3,7 +3,7 @@
 # machete-post-slide-out hook is invoked by 'git machete slide-out' and 'git machete traverse' after a branch
 # (or possibly multiple branches, in case of 'slide-out') of are slid out.
 
-# This sample retargets all GitHub PRs (using 'hub' CLI and Github API - authorizing with the token from ~/.config/hub)
+# This sample retargets all GitHub PRs (using 'hub' CLI and GitHub API - authorizing with the token from ~/.config/hub)
 # whose current base is the slid-out branch, so that the new upstream becomes their new base.
 
 # Note: since around May 2020, GitHub automatically retargets a PR Z->Y to Z->X once any PR Y->X (for some X) is merged.
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index edb2250..b8fbd65 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -9,10 +9,7 @@ description: |
 # This software is unlikely to be used outside of desktop machines.
 # Let's limit the number of target architectures to reduce the build times.
 architectures:
-  # See https://forum.snapcraft.io/t/how-can-i-publish-a-snap-that-can-provide-binaries-for-all-the-supported-architectures/8556/4
-  # on why not just `build-on: [ amd64, i386 ]`.
-  - build-on: amd64
-  - build-on: i386
+  - build-on: [ amd64, i386 ]
 base: core18
 # See https://forum.snapcraft.io/t/-/15566 for discussion on why classic confinement was necessary.
 confinement: classic
@@ -23,17 +20,15 @@ license: MIT
 parts:
   git-machete:
     plugin: python
-    python-version: python3
     source: .
     # We're setting the version via scriptlet rather than using `version: git`
     # since only pushing commits to master (and not pushing tags) triggers Snapcraft builds.
-    override-pull: |
+    override-build: |
       set -x
-      snapcraftctl pull
-      version=$(grep "__version__ = " git_machete/__init__.py | cut -d\' -f2)
+      snapcraftctl build
+      version="$(python3 setup.py --version)"
       snapcraftctl set-version "$version"
 
 apps:
   git-machete:
     command: bin/git-machete
-    adapter: full
diff --git a/tox.ini b/tox.ini
index bc416e2..5be3858 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,9 +10,7 @@ setenv =
 usedevelop = False
 install_command =
   pip install {opts} {packages}
-deps =
-  mypy
-  pytest
+deps = pytest
 allowlist_externals = mkdir
 commands =
   mkdir -p test-results/
@@ -22,9 +20,7 @@ commands =
 deps = flake8
 commands = flake8
 
-[testenv:venv]
-commands = {posargs}
-
+# Set flake8 configuration options which are used by the `flake8` command in [testenv:pep8]
 [flake8]
 # E501 ignore line longer than 80
 # W504 ignore wrap line after binary operator
@@ -33,6 +29,9 @@ show-source = True
 exclude = ./.*,build,dist,*egg,venv
 import-order-style = pep8
 
+[testenv:venv]
+commands = {posargs}
+
 [testenv:coverage]
 description = "Check the test coverage of the code"
 deps =


Test patch:
diff --git a/.stestr.conf b/.stestr.conf
deleted file mode 100644
index 0a53935..0000000
--- a/.stestr.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[DEFAULT]
-test_path=${OS_TEST_PATH:-./git_machete/tests/functional}
-top_dir=./
diff --git a/git_machete/tests/functional/test_machete.py b/git_machete/tests/functional/test_machete.py
index 0a155aa..b4fde6b 100644
--- a/git_machete/tests/functional/test_machete.py
+++ b/git_machete/tests/functional/test_machete.py
@@ -50,8 +50,7 @@ def mock_exit_script(status_code: Optional[int] = None, error: Optional[BaseExce
 
 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())
-    git.checkout(branch)
+    git.create_branch(branch, get_current_commit_hash(), switch_head=True)
 
 
 def mock_run_cmd(cmd: str, *args: str, **kwargs: Any) -> int:
@@ -86,6 +85,14 @@ def mock__get_github_token() -> Optional[str]:
     return None
 
 
+def mock__get_github_token_fake() -> Optional[str]:
+    return 'token'
+
+
+def mock_should_perform_interactive_slide_out(cmd: str) -> bool:
+    return True
+
+
 class FakeCommandLineOptions(CommandLineOptions):
     def __init__(self) -> None:
         super().__init__()
@@ -93,14 +100,14 @@ class FakeCommandLineOptions(CommandLineOptions):
         self.opt_yes: bool = True
 
 
-class MockGithubAPIState:
+class MockGitHubAPIState:
     def __init__(self, pulls: List[Dict[str, Any]], issues: List[Dict[str, Any]] = None) -> None:
         self.pulls: List[Dict[str, Any]] = pulls
         self.user: Dict[str, str] = {'login': 'other_user', 'type': 'User', 'company': 'VirtusLab'}  # login must be different from the one used in pull requests, otherwise pull request author will not be annotated
         self.issues: List[Dict[str, Any]] = issues or []
 
-    def new_request(self) -> "MockGithubAPIRequest":
-        return MockGithubAPIRequest(self)
+    def new_request(self) -> "MockGitHubAPIRequest":
+        return MockGitHubAPIRequest(self)
 
     def get_issue(self, issue_no: str) -> Optional[Dict[str, Any]]:
         for issue in self.issues:
@@ -115,7 +122,7 @@ class MockGithubAPIState:
         return None
 
 
-class MockGithubAPIResponse:
+class MockGitHubAPIResponse:
     def __init__(self, status_code: int, response_data: Union[List[Dict[str, Any]], Dict[str, Any]]) -> None:
         self.response_data: Union[List[Dict[str, Any]], Dict[str, Any]] = response_data
         self.status_code: int = status_code
@@ -124,11 +131,11 @@ class MockGithubAPIResponse:
         return json.dumps(self.response_data).encode()
 
 
-class MockGithubAPIRequest:
-    def __init__(self, github_api_state: MockGithubAPIState) -> None:
-        self.github_api_state: MockGithubAPIState = github_api_state
+class MockGitHubAPIRequest:
+    def __init__(self, github_api_state: MockGitHubAPIState) -> None:
+        self.github_api_state: MockGitHubAPIState = github_api_state
 
-    def __call__(self, url: str, headers: Dict[str, str] = None, data: Union[str, bytes, None] = None, method: str = '') -> "MockGithubAPIResponse":
+    def __call__(self, url: str, headers: Dict[str, str] = None, data: Union[str, bytes, None] = None, method: str = '') -> "MockGitHubAPIResponse":
         self.parsed_url: ParseResult = urlparse(url, allow_fragments=True)
         self.parsed_query: Dict[str, List[str]] = parse_qs(self.parsed_url.query)
         self.json_data: Union[str, bytes] = data
@@ -136,7 +143,7 @@ class MockGithubAPIRequest:
         self.headers: Dict[str, str] = headers
         return self.handle_method(method)
 
-    def handle_method(self, method: str) -> "MockGithubAPIResponse":
+    def handle_method(self, method: str) -> "MockGitHubAPIResponse":
         if method == "GET":
             return self.handle_get()
         elif method == "PATCH":
@@ -146,7 +153,7 @@ class MockGithubAPIRequest:
         else:
             return self.make_response_object(HTTPStatus.METHOD_NOT_ALLOWED, [])
 
-    def handle_get(self) -> "MockGithubAPIResponse":
+    def handle_get(self) -> "MockGitHubAPIResponse":
         if 'pulls' in self.parsed_url.path:
             full_head_name: Optional[List[str]] = self.parsed_query.get('head')
             number: Optional[str] = self.find_number(self.parsed_url.path, 'pulls')
@@ -168,7 +175,7 @@ class MockGithubAPIRequest:
         else:
             return self.make_response_object(HTTPStatus.NOT_FOUND, [])
 
-    def handle_patch(self) -> "MockGithubAPIResponse":
+    def handle_patch(self) -> "MockGitHubAPIResponse":
         if 'issues' in self.parsed_url.path:
             return self.update_issue()
         elif 'pulls' in self.parsed_url.path:
@@ -176,7 +183,7 @@ class MockGithubAPIRequest:
         else:
             return self.make_response_object(HTTPStatus.NOT_FOUND, [])
 
-    def handle_post(self) -> "MockGithubAPIResponse":
+    def handle_post(self) -> "MockGitHubAPIResponse":
         assert not self.parsed_query
         if 'issues' in self.parsed_url.path:
             return self.update_issue()
@@ -185,7 +192,7 @@ class MockGithubAPIRequest:
         else:
             return self.make_response_object(HTTPStatus.NOT_FOUND, [])
 
-    def update_pull_request(self) -> "MockGithubAPIResponse":
+    def update_pull_request(self) -> "MockGitHubAPIResponse":
         pull_no: str = self.find_number(self.parsed_url.path, 'pulls')
         if not pull_no:
             if self.is_pull_created():
@@ -195,7 +202,7 @@ class MockGithubAPIRequest:
         pull: Dict[str, Any] = self.github_api_state.get_pull(pull_no)
         return self.fill_pull_request_data(json.loads(self.json_data), pull)
 
-    def create_pull_request(self) -> "MockGithubAPIResponse":
+    def create_pull_request(self) -> "MockGitHubAPIResponse":
         pull = {'number': self.get_next_free_number(self.github_api_state.pulls),
                 'user': {'login': 'github_user'},
                 'html_url': 'www.github.com',
@@ -204,7 +211,7 @@ class MockGithubAPIRequest:
                 'base': {'ref': ""}}
         return self.fill_pull_request_data(json.loads(self.json_data), pull)
 
-    def fill_pull_request_data(self, data: Dict[str, Any], pull: Dict[str, Any]) -> "MockGithubAPIResponse":
+    def fill_pull_request_data(self, data: Dict[str, Any], pull: Dict[str, Any]) -> "MockGitHubAPIResponse":
         index = self.get_index_or_none(pull, self.github_api_state.issues)
         for key in data.keys():
             if key in ('base', 'head'):
@@ -217,18 +224,18 @@ class MockGithubAPIRequest:
             self.github_api_state.pulls.append(pull)
         return self.make_response_object(HTTPStatus.CREATED, pull)
 
-    def update_issue(self) -> "MockGithubAPIResponse":
+    def update_issue(self) -> "MockGitHubAPIResponse":
         issue_no: str = self.find_number(self.parsed_url.path, 'issues')
         if not issue_no:
             return self.create_issue()
         issue: Dict[str, Any] = self.github_api_state.get_issue(issue_no)
         return self.fill_issue_data(json.loads(self.json_data), issue)
 
-    def create_issue(self) -> "MockGithubAPIResponse":
+    def create_issue(self) -> "MockGitHubAPIResponse":
         issue = {'number': self.get_next_free_number(self.github_api_state.issues)}
         return self.fill_issue_data(json.loads(self.json_data), issue)
 
-    def fill_issue_data(self, data: Dict[str, Any], issue: Dict[str, Any]) -> "MockGithubAPIResponse":
+    def fill_issue_data(self, data: Dict[str, Any], issue: Dict[str, Any]) -> "MockGitHubAPIResponse":
         index = self.get_index_or_none(issue, self.github_api_state.issues)
         for key in data.keys():
             issue[key] = data[key]
@@ -257,8 +264,8 @@ class MockGithubAPIRequest:
             return None
 
     @staticmethod
-    def make_response_object(status_code: int, response_data: Union[List[Dict[str, Any]], Dict[str, Any]]) -> "MockGithubAPIResponse":
-        return MockGithubAPIResponse(status_code, response_data)
+    def make_response_object(status_code: int, response_data: Union[List[Dict[str, Any]], Dict[str, Any]]) -> "MockGitHubAPIResponse":
+        return MockGitHubAPIResponse(status_code, response_data)
 
     @staticmethod
     def find_number(url: str, entity: str) -> Optional[str]:
@@ -285,10 +292,10 @@ class MockHTTPError(HTTPError):
 
 
 class MockContextManager:
-    def __init__(self, obj: MockGithubAPIResponse) -> None:
+    def __init__(self, obj: MockGitHubAPIResponse) -> None:
         self.obj = obj
 
-    def __enter__(self) -> MockGithubAPIResponse:
+    def __enter__(self) -> MockGitHubAPIResponse:
         if self.obj.status_code == HTTPStatus.NOT_FOUND:
             raise HTTPError(None, 404, 'Not found', None, None)
         elif self.obj.status_code == HTTPStatus.UNPROCESSABLE_ENTITY:
@@ -380,7 +387,7 @@ class MacheteTester(unittest.TestCase):
 
     @staticmethod
     def rewrite_definition_file(new_body: str) -> None:
-        definition_file_path = git.get_git_subpath("machete")
+        definition_file_path = git.get_main_git_subpath("machete")
         with open(os.path.join(os.getcwd(), definition_file_path), 'w') as def_file:
             def_file.writelines(new_body)
 
@@ -1826,11 +1833,11 @@ class MacheteTester(unittest.TestCase):
         parents_new_commit_hash = get_current_commit_hash()
         self.repo_sandbox.check_out("level-1-branch")
         self.launch_command("update", "--no-interactive-rebase")
-        new_forkpoint_hash = self.launch_command("fork-point").strip()
+        new_fork_point_hash = self.launch_command("fork-point").strip()
 
         self.assertEqual(
             parents_new_commit_hash,
-            new_forkpoint_hash,
+            new_fork_point_hash,
             msg="Verify that 'git machete update --no-interactive-rebase' perform"
                 "'git rebase' to the parent branch of the current branch."
         )
@@ -1865,12 +1872,12 @@ class MacheteTester(unittest.TestCase):
 
         self.launch_command(
             "update", "--no-interactive-rebase", "-f", branch_second_commit_hash)
-        new_forkpoint_hash = self.launch_command("fork-point").strip()
+        new_fork_point_hash = self.launch_command("fork-point").strip()
         branch_history = popen('git log -10 --oneline')
 
         self.assertEqual(
             roots_second_commit_hash,
-            new_forkpoint_hash,
+            new_fork_point_hash,
             msg="Verify that 'git machete update --no-interactive-rebase -f "
                 "<commit_hash>' performs 'git rebase' to the upstream branch."
         )
@@ -1891,7 +1898,7 @@ class MacheteTester(unittest.TestCase):
                 "specified by the option '-f' from the current branch."
         )
 
-    git_api_state_for_test_retarget_pr = MockGithubAPIState(
+    git_api_state_for_test_retarget_pr = MockGitHubAPIState(
         [{'head': {'ref': 'feature', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'root'}, 'number': '15',
           'html_url': 'www.github.com', 'state': 'open'}])
 
@@ -1919,7 +1926,7 @@ class MacheteTester(unittest.TestCase):
         self.assert_command(['github', 'retarget-pr'], 'The base branch of PR #15 has been switched to `branch-1`\n', strip_indentation=False)
         self.assert_command(['github', 'retarget-pr'], 'The base branch of PR #15 is already `branch-1`\n', strip_indentation=False)
 
-    git_api_state_for_test_anno_prs = MockGithubAPIState([
+    git_api_state_for_test_anno_prs = MockGitHubAPIState([
         {'head': {'ref': 'ignore-trailing', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'hotfix/add-trigger'}, 'number': '3', 'html_url': 'www.github.com', 'state': 'open'},
         {'head': {'ref': 'allow-ownership-link', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'develop'}, 'number': '7', 'html_url': 'www.github.com', 'state': 'open'},
         {'head': {'ref': 'call-ws', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'develop'}, 'number': '31', 'html_url': 'www.github.com', 'state': 'open'}
@@ -1993,7 +2000,7 @@ class MacheteTester(unittest.TestCase):
             """,
         )
 
-    git_api_state_for_test_create_pr = MockGithubAPIState([{'head': {'ref': 'ignore-trailing', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'hotfix/add-trigger'}, 'number': '3', 'html_url': 'www.github.com', 'state': 'open'}],
+    git_api_state_for_test_create_pr = MockGitHubAPIState([{'head': {'ref': 'ignore-trailing', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'hotfix/add-trigger'}, 'number': '3', 'html_url': 'www.github.com', 'state': 'open'}],
                                                           issues=[{'number': '4'}, {'number': '5'}, {'number': '6'}])
 
     @mock.patch('git_machete.cli.exit_script', mock_exit_script)
@@ -2161,7 +2168,7 @@ class MacheteTester(unittest.TestCase):
             self.assertEqual(e.exception.parameter, expected_error_message,
                              'Verify that expected error message has appeared when creating PR from root branch.')
 
-    git_api_state_for_test_create_pr_missing_base_branch_on_remote = MockGithubAPIState([{'head': {'ref': 'chore/redundant_checks', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'restrict_access'}, 'number': '18', 'html_url': 'www.github.com', 'state': 'open'}])
+    git_api_state_for_test_create_pr_missing_base_branch_on_remote = MockGitHubAPIState([{'head': {'ref': 'chore/redundant_checks', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'restrict_access'}, 'number': '18', 'html_url': 'www.github.com', 'state': 'open'}])
 
     # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_create_pr` due to `git fetch` executed by `create-pr` subcommand.
     @mock.patch('git_machete.github.GITHUB_REMOTE_PATTERNS', FAKE_GITHUB_REMOTE_PATTERNS)
@@ -2203,7 +2210,7 @@ class MacheteTester(unittest.TestCase):
             """,
         )
 
-    git_api_state_for_test_checkout_prs = MockGithubAPIState([
+    git_api_state_for_test_checkout_prs = MockGitHubAPIState([
         {'head': {'ref': 'chore/redundant_checks', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'restrict_access'}, 'number': '18', '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': 'allow-ownership-link', 'repo': mock_repository_info}, 'user': {'login': 'github_user'}, 'base': {'ref': 'bugfix/feature'}, 'number': '12', 'html_url': 'www.github.com', 'state': 'open'},
@@ -2216,13 +2223,14 @@ 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('git_machete.github.__get_github_token', mock__get_github_token)
     @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 +2378,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
@@ -2381,6 +2424,17 @@ class MacheteTester(unittest.TestCase):
             self.assertEqual(e.exception.parameter, expected_error_message,
                              'Verify that expected error message has appeared when given pull request to checkout does not exists.')
 
+        with self.assertRaises(MacheteException) as e:
+            self.launch_command('github', 'checkout-prs', '19', '100')
+        if e:
+            self.assertEqual(e.exception.parameter, expected_error_message,
+                             'Verify that expected error message has appeared when one of the given pull requests to checkout does not exists.')
+
+        # check against user with no open pull requests
+        expected_msg = ("Checking for open GitHub PRs...\n"
+                        f"Warn: User `tester` has no open pull request in repository `{org}/{repo}`\n")
+        self.assert_command(['github', 'checkout-prs', '--by', 'tester'], expected_msg, strip_indentation=False)
+
         # Check against closed pull request with head branch deleted from remote
         local_path = popen("mktemp -d")
         self.repo_sandbox.new_repo(GitRepositorySandbox.second_remote_path)
@@ -2410,17 +2464,18 @@ class MacheteTester(unittest.TestCase):
          )
         os.chdir(self.repo_sandbox.local_path)
 
-        expected_msg = ("Fetching origin...\n"
-                        "Fetching testing...\n"
+        expected_msg = ("Checking for open GitHub PRs...\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"
                         "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 = 'Checking for open GitHub PRs...\n'
+
+        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 +2483,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,16 +2537,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"
+        expected_msg = ("Checking for open GitHub PRs...\n"
                         "Pull request `#2` checked out at local branch `comments/add_docstrings`\n")
         self.assert_command(
             ['github', 'checkout-prs', '2'],
@@ -2514,12 +2560,10 @@ class MacheteTester(unittest.TestCase):
 
         # Check against closed pull request
         self.repo_sandbox.execute('git branch -D sphinx_export')
-        expected_msg = ("Fetching origin...\n"
+        expected_msg = ("Checking for open GitHub PRs...\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"
                         "Pull request `#23` checked out at local branch `sphinx_export`\n")
+
         self.assert_command(
             ['github', 'checkout-prs', '23'],
             expected_msg,
@@ -2540,7 +2584,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 +2592,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 +2607,9 @@ class MacheteTester(unittest.TestCase):
             .push()
         )
         self.launch_command('discover')
-        expected_msg = ("Fetching origin...\n"
+        expected_msg = ("Checking for open GitHub PRs...\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"
                         "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'],
@@ -2582,6 +2624,74 @@ class MacheteTester(unittest.TestCase):
                 "the head branch of given pull request."
         )
 
+    git_api_state_for_test_github_sync = MockGitHubAPIState([
+        {'head': {'ref': 'snickers', 'repo': mock_repository_info}, 'user': {'login': 'other_user'},
+         'base': {'ref': 'master'}, 'number': '7', 'html_url': 'www.github.com', 'state': 'open'}
+    ])
+
+    @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_PATTERNS', FAKE_GITHUB_REMOTE_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:
+        (
+            self.repo_sandbox
+                .new_branch('master')
+                .commit()
+                .push()
+                .new_branch('bar')
+                .commit()
+                .new_branch('bar2')
+                .commit()
+                .check_out("master")
+                .new_branch('foo')
+                .commit()
+                .push()
+                .new_branch('foo2')
+                .commit()
+                .check_out("master")
+                .new_branch('moo')
+                .commit()
+                .new_branch('moo2')
+                .commit()
+                .check_out("master")
+                .new_branch('snickers')
+                .push()
+        )
+        self.launch_command('discover', '-y')
+        (
+            self.repo_sandbox
+                .check_out("master")
+                .new_branch('mars')
+                .commit()
+                .check_out("master")
+        )
+        self.launch_command('github', 'sync')
+
+        expected_status_output = (
+            """
+            master *
+            |
+            o-bar (untracked)
+            |
+            o-foo
+            |
+            o-moo (untracked)
+            |
+            o-snickers  PR #7
+            """
+        )
+        self.assert_command(['status'], expected_status_output)
+
+        with self.assertRaises(
+                subprocess.CalledProcessError,
+                msg="Verify that 'git checkout mars' raises an error when branch mars is no longer present in git."):
+            self.repo_sandbox.check_out("mars")
+
     def test_squash_with_valid_fork_point(self) -> None:
         (
             self.repo_sandbox.new_branch('branch-0')
@@ -2780,3 +2890,55 @@ class MacheteTester(unittest.TestCase):
         self.assert_command(['add', '--onto=feature'],
                             'Added branch `chore/remove_indentation` onto `feature`\n',
                             strip_indentation=False)
+
+    @mock.patch('git_machete.client.MacheteClient.should_perform_interactive_slide_out', mock_should_perform_interactive_slide_out)
+    @mock.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
+    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)
+    def test_clean(self) -> None:
+        (
+            self.repo_sandbox.new_branch('master')
+                .commit()
+                .push()
+                .new_branch('bar')
+                .commit()
+                .new_branch('bar2')
+                .commit()
+                .check_out("master")
+                .new_branch('foo')
+                .commit()
+                .push()
+                .new_branch('foo2')
+                .commit()
+                .check_out("master")
+                .new_branch('moo')
+                .commit()
+                .new_branch('moo2')
+                .commit()
+        )
+        self.launch_command('discover')
+        (
+            self.repo_sandbox
+                .check_out("master")
+                .new_branch('mars')
+                .commit()
+                .check_out("master")
+        )
+        self.launch_command('clean')
+
+        expected_status_output = (
+            """
+            master *
+            |
+            o-bar (untracked)
+            |
+            o-foo
+            |
+            o-moo (untracked)
+            """
+        )
+        self.assert_command(['status'], expected_status_output)
+
+        with self.assertRaises(
+                subprocess.CalledProcessError,
+                msg="Verify that 'git checkout mars' raises an error when branch mars is no longer present in git."):
+            self.repo_sandbox.check_out("mars")