Repository: VirtusLab/git-machete
Pull request #339 (2021-11-09 06:57:08)

Release v3.5.0


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 972d2e5..02170d2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -14,6 +14,7 @@ mandatory_jobs: &mandatory_jobs
   - test against python 3_7 and git 2_7_6
   - test against python 3_8 and git 2_25_0 with PEP8 checks
   - test against python 3_9 and git 2_30_1 with coverage report and docs creation
+  - test against python 3_10 and git 2_33_1
 
 jobs:
   "initial checks":
@@ -25,7 +26,7 @@ jobs:
           command: |
             # For ensure-completion-scripts-correct.sh
             sudo apt-get update
-            sudo apt-get install zsh
+            sudo apt-get install zsh fish
 
             # For ensure-links-correct.sh
             npm install --global remark-cli remark-lint-no-dead-urls remark-validate-links
@@ -89,7 +90,15 @@ jobs:
           path: docs/html
           destination: docs
 
-  dry_run_deploy_from_develop:
+  "test against python 3_10 and git 2_33_1":
+    executor: ubuntu_executor
+    steps:
+      - checkout
+      - run:
+          name: Run tests against Python 3.10 and git 2.33.1
+          command: PYTHON_VERSION=3.10 GIT_VERSION=2.33.1 bash ci/tox/ci-run.sh
+
+  "dry run deploy":
     executor: ubuntu_executor
     steps:
       - checkout
@@ -108,13 +117,23 @@ jobs:
           # Side effects: potentially pushes a regenerated Docker image
           command: bash ci/deb-ppa-upload/ci-deploy.sh --dry-run
 
+      - deploy:
+          name: '[Dry run] Build .snap for linux distros'
+          # Build snap for current branch of git-machete repository without publishing results to snapstore.
+          # --use-lxd applied to use a LXD container instead of a VM, to work around lack of support for KVM on CircleCI VMs.
+          command: |
+            sudo apt-get update
+            sudo apt-get install -y snapd
+            sudo snap install snapcraft --classic
+            snapcraft --use-lxd
+
       - deploy:
           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
           command: bash ci/rpm/ci-run.sh
 
-  deploy_from_master:
+  "deploy from master":
     executor: ubuntu_executor
     steps:
       - checkout
@@ -161,13 +180,16 @@ workflows:
       - test against python 3_7 and git 2_7_6
       - test against python 3_8 and git 2_25_0 with PEP8 checks
       - test against python 3_9 and git 2_30_1 with coverage report and docs creation
-      - dry_run_deploy_from_develop:
+      - test against python 3_10 and git 2_33_1
+      - dry run deploy:
           filters:
             branches:
-              only: "develop"
+              only:
+                - "develop"
+                - "/hotfix.*/"
           requires:
             *mandatory_jobs
-      - deploy_from_master:
+      - deploy from master:
           filters:
             branches:
               only: "master"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a0fc648..8736d6d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -60,6 +60,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`.
 
 **Who closes Github comments? Author of changes, reviewer or initiator of the conversation?**
 
diff --git a/README.md b/README.md
index ddcfefb..44718f1 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 [![Join the chat at https://gitter.im/VirtusLab/git-machete](https://badges.gitter.im/VirtusLab/git-machete.svg)](https://gitter.im/VirtusLab/git-machete)
 [![CircleCI](https://circleci.com/gh/VirtusLab/git-machete/tree/master.svg?style=shield)](https://app.circleci.com/pipelines/github/VirtusLab/git-machete?branch=master)
-[![Read the Docs](https://readthedocs.org/projects/git-machete/badge/?version=latest)](https://git-machete.readthedocs.io/en/latest)
+[![Read the Docs](https://readthedocs.org/projects/git-machete/badge/?version=latest)](https://git-machete.readthedocs.io/en/stable)
 [![PyPI package](https://img.shields.io/pypi/v/git-machete.svg)](https://pypi.org/project/git-machete)
 [![PyPI package monthly downloads](https://img.shields.io/pypi/dm/git-machete.svg)](https://pypistats.org/packages/git-machete)
 [![Snap](https://snapcraft.io/git-machete/badge.svg)](https://snapcraft.io/git-machete)
@@ -39,7 +39,7 @@ a port into a plugin for the IntelliJ Platform products, including PyCharm, WebS
 
 We suggest a couple of alternative ways of installation.
 
-**Instructions for installing bash and zsh completion scripts are provided in [completion/README.md](completion/README.md).**
+**Instructions for installing bash, zsh, and fish completion scripts are provided in [completion/README.md](completion/README.md).**
 
 git-machete requires Python >= 3.6. Python 2.x is no longer supported.
 
@@ -174,14 +174,15 @@ git machete github retarget-pr
 **Note**: for private repositories, a GitHub API token with `repo` access is required.
 This will be resolved from the first of:
 1. The `GITHUB_TOKEN` env var.
-2. The auth token from the current [`gh`](https://cli.github.com/) configuration.
-3. The auth token from the current [`hub`](https://github.com/github/hub) configuration.
+2. The content of the .github-token file in the home directory (`~`). This file has to be manually created by the user.
+3. The auth token from the current [`gh`](https://cli.github.com/) configuration.
+4. The auth token from the current [`hub`](https://github.com/github/hub) configuration.
 
 <br/>
 
 ## Reference
 
-Find the docs at [Read the Docs](https://git-machete.readthedocs.io/en/latest).
+Find the docs at [Read the Docs](https://git-machete.readthedocs.io/).
 You can also check `git machete help` and `git machete help <command>`.
 
 Take a look at
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 5681910..4cb7056 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,11 @@
 # Release notes
 
+## New in git-machete 3.5.0
+
+- added: new way of acquiring the github token (from `~/.github-token`)
+- fixed: `--fork-point` and `--down-fork-point` options values have to be ancestor of the current branch
+- added: fish shell completions (contributed by @kgadek)
+
 ## New in git-machete 3.4.1
 
 - fixed: wrong logo path in Snapcraft config
diff --git a/ci/checks/enforce-indent-two-spaces-outside-python.sh b/ci/checks/enforce-indent-two-spaces-outside-python.sh
index f04554e..f3ce1e5 100755
--- a/ci/checks/enforce-indent-two-spaces-outside-python.sh
+++ b/ci/checks/enforce-indent-two-spaces-outside-python.sh
@@ -22,5 +22,5 @@ awk_script="$(cat <<'EOF'
 EOF
 )"
 
-git ls-files ':!/.circleci/config.yml' ':!*/Dockerfile' ':!graphics/setup-sandbox' ':!docs/*' ':!*.md' ':!*.py' ':!*.svg' ':!*.png' \
+git ls-files ':!/.circleci/config.yml' ':!*/Dockerfile' ':!docs/*' ':!graphics/setup-sandbox' ':!*.md' ':!*.png' ':!*.py' ':!*.svg' \
   | xargs awk "$awk_script"
diff --git a/ci/checks/ensure-completion-scripts-correct.sh b/ci/checks/ensure-completion-scripts-correct.sh
index fdbee9b..f73b80a 100755
--- a/ci/checks/ensure-completion-scripts-correct.sh
+++ b/ci/checks/ensure-completion-scripts-correct.sh
@@ -4,3 +4,4 @@ set -e -o pipefail -u
 
 bash completion/git-machete.completion.bash
 zsh  completion/git-machete.completion.zsh
+fish completion/git-machete.fish
diff --git a/completion/README.md b/completion/README.md
index a45eba9..cee9209 100644
--- a/completion/README.md
+++ b/completion/README.md
@@ -92,3 +92,19 @@ autoload -Uz compinit && compinit -i
 ```shell script
 exec $SHELL -l
 ```
+
+## Fish
+
+### Mac (via Homebrew)
+Please look at the section about [installation via Homebrew](../README.md#using-homebrew-macos).
+``brew install git-machete`` automatically installs fish completion files for ``git machete``.
+
+### Linux
+
+Place the completion script in `/path/to/fish/completions/` (typically `~/.config/fish/completions/git-machete.fish`).
+
+```shell script
+mkdir -p ~/.config/fish/completions
+curl -L https://raw.githubusercontent.com/VirtusLab/git-machete/develop/completion/git-machete.fish -o ~/.config/fish/completions/git-machete.fish
+echo "source ~/.config/fish/completions/git-machete.fish >/dev/null" >> ~/.config/fish/config.fish
+```
diff --git a/completion/git-machete.completion.bash b/completion/git-machete.completion.bash
index 9a0b6f3..92dad3c 100644
--- a/completion/git-machete.completion.bash
+++ b/completion/git-machete.completion.bash
@@ -61,7 +61,11 @@ _git_machete() {
       esac ;;
     *)
       if [[ $COMP_CWORD -eq 2 ]]; then
-        __gitcomp "$cmds"
+        if [[ $cmds =~ ^$cur ]] || [[ $cmds =~ ( $cur) ]]; then
+          __gitcomp "$cmds"
+        else
+          COMPREPLY=('')
+        fi
       else
         local prev=${COMP_WORDS[COMP_CWORD-1]}
         case $prev in
diff --git a/completion/git-machete.fish b/completion/git-machete.fish
new file mode 100644
index 0000000..e609916
--- /dev/null
+++ b/completion/git-machete.fish
@@ -0,0 +1,180 @@
+# This file will be loaded by git.fish completions and also provide all __fish_git_* functions here.
+#
+# Caveat:
+# > if the user types git-machete, then only git-machete.fish will be loaded, but you can trick
+# > fish into still loading git.fish by adding a complete --do-complete="git " at the top
+# - https://gitter.im/fish-shell/fish-shell?at=6176a4bacd4972068b6a0804
+complete --do-complete="git "
+
+set -l __mcht_help_topics format hooks
+set -l __mcht_commands_long add advance anno delete-unmanaged diff discover edit file fork-point \
+  github go help is-managed list log reapply show slide-out squash status traverse update version
+set -l __mcht_commands_short d e g l s
+set -l __mcht_commands $__mcht_commands_long $__mcht_commands_short
+
+# git
+complete -c git -n "__fish_git_needs_command" -f -a machete -d 'Tool for managing git workflows'
+complete -c git -n "__fish_git_using_command machete" -f # (suppress file completion)
+
+# git machete (general options)
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands; and not __fish_seen_subcommand_from --help -h" -f -s h -d 'Print help and exit'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from $__mcht_commands;     and not __fish_seen_subcommand_from --help -h" -f -l help -s h -d 'Print help and exit'
+#     Parameters --help work for subcommands only. ---^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+#     See: https://github.com/VirtusLab/git-machete/issues/25
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from --verbose -v" -f -l verbose -s v -d 'Log the executed git commands'
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from --debug"      -f -l debug        -d 'Log detailed diagnostic info, including outputs of the executed git commands'
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from --version"    -f -l version      -d 'Print version and exit'
+
+# git machete add
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands"                                            -f                 -a add                           -d 'Add a branch to the tree of branch dependencies'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --as-root -R"           -x -l onto    -s o -a '(__fish_git_local_branches)' -d 'Specifies the target parent branch to add the given branch onto. Cannot be specified together with -R/--as-root'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --onto -o --as-root -R" -f -l as-root -s R                                  -d 'Add the given branch as a new root (and not onto any other branch). Cannot be specified together with -o/--onto'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --yes -y"               -f -l yes     -s y                                  -d 'Don\'t ask for confirmation whether to create the branch or whether to add onto the inferred upstream'
+
+# git machete advance
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands"                                  -f -a advance  -d 'Fast-forward merge one of children to the current branch and then slide out this child'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from advance; and not __fish_seen_subcommand_from --yes -y" -f -l yes -s y -d 'Don\'t ask for confirmation whether to fast-forward the current branch or whether to slide-out the downstream. Fails if the current branch has more than one green-edge downstream branch'
+
+# git machete anno
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands"                                           -f                         -a anno                          -d 'Manage custom annotations'
+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 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'
+
+# git machete discover
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands"                                                 -f                           -a discover                      -d 'Automatically discover tree of branch dependencies'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from discover; and not __fish_seen_subcommand_from --checked-out-since -C" -x -l checked-out-since -s C                                  -d 'Only consider branches checked out at least once since the given date. <date> can be e.g. 2 weeks ago or 2020-06-01, as in git log --since=<date>. If not present, the date is selected automatically so that around 10 branches are included'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from discover; and not __fish_seen_subcommand_from --list-commits -l"      -f -l list-commits      -s l                                  -d 'When printing the discovered tree, additionally lists the messages of commits introduced on each branch (as for git machete status)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from discover"                                                             -x -l roots             -s r -a '(__fish_git_local_branches)' -d 'Comma-separated list of branches that should be considered roots of trees of branch dependencies. If not present, master is assumed to be a root. Note that in the process of discovery, certain other branches can also be additionally deemed to be roots as well'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from discover; and not __fish_seen_subcommand_from --yes -y"               -f -l yes               -s y                                  -d 'Don\'t ask for confirmation before saving the newly-discovered tree. Mostly useful in scripts; not recommended for manual use'
+
+# git machete diff
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands"                                  -f -a diff      -d 'Diff current working directory or a given branch against its computed fork point'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from diff d; and not __fish_seen_subcommand_from --stat -s" -f -l stat -s s -d 'Makes git machete diff pass --stat option to git diff, so that only summary (diffstat) is printed'
+
+# git machete edit
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a edit -d 'Edit the definition file'
+
+# git machete file
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a file -d 'Display the location of the definition file'
+
+# git machete fork-point
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a fork-point -d 'Display or override fork point for a branch'
+# all forms
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from fork-point" -f -a '(__fish_git_local_branches)'
+# form 1
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from fork-point; and not __fish_seen_subcommand_from --inferred --unset-override --override-to --override-to-inferred --override-to-parent" -f -l inferred
+# form 2
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from fork-point; and not __fish_seen_subcommand_from --inferred --unset-override --override-to --override-to-inferred --override-to-parent" -x -l override-to
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from fork-point; and not __fish_seen_subcommand_from --inferred --unset-override --override-to --override-to-inferred --override-to-parent" -f -l override-to-inferred
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from fork-point; and not __fish_seen_subcommand_from --inferred --unset-override --override-to --override-to-inferred --override-to-parent" -f -l override-to-parent
+# form 3
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from fork-point; and not __fish_seen_subcommand_from --inferred --unset-override --override-to --override-to-inferred --override-to-parent" -f -l unset-override
+
+# 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)'
+
+# 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'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from go g; and not __fish_seen_subcommand_from down d first f last l next n prev p root r up u" -f -a down  -d 'the direct children/downstream branch of the current branch'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from go g; and not __fish_seen_subcommand_from down d first f last l next n prev p root r up u" -f -a first -d 'the first downstream of the root branch of the current branch (like root followed by next), or the root branch itself if the root has no downstream branches'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from go g; and not __fish_seen_subcommand_from down d first f last l next n prev p root r up u" -f -a last  -d 'the last branch in the definition file that has the same root as the current branch; can be the root branch itself if the root has no downstream branches'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from go g; and not __fish_seen_subcommand_from down d first f last l next n prev p root r up u" -f -a next  -d 'the direct successor of the current branch in the definition file'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from go g; and not __fish_seen_subcommand_from down d first f last l next n prev p root r up u" -f -a prev  -d 'the direct predecessor of the current branch in the definition file'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from go g; and not __fish_seen_subcommand_from down d first f last l next n prev p root r up u" -f -a root  -d 'the root of the tree where the current branch is located. Note: this will typically be something like develop or master, since all branches are usually meant to be ultimately merged to one of those'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from go g; and not __fish_seen_subcommand_from down d first f last l next n prev p root r up u" -f -a up    -d 'the direct parent/upstream branch of the current branch'
+
+# git machete help
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a help -d 'Display overview, or detailed help for a specified command'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from help" -f -a "$__mcht_help_topics"
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from help" -f -a "(complete -C 'git machete ')"
+
+# git machete is-managed
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a is-managed -d 'Check if the current branch is managed by git machete (mostly for scripts)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from is-managed"           -f -a '(__fish_git_local_branches)'
+
+# git machete list
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a list -d 'List all branches that fall into one of pre-defined categories (mostly for internal use)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from addable managed slidable slidable-after unmanaged with-overridden-fork-point" -f -a addable                    -d 'all branches (local or remote) than can be added to the definition file'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from addable managed slidable slidable-after unmanaged with-overridden-fork-point" -f -a managed                    -d 'all branches that appear in the definition file'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from addable managed slidable slidable-after unmanaged with-overridden-fork-point" -f -a slidable                   -d 'all managed branches that have an upstream and can be slid out with slide-out command'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from addable managed slidable slidable-after unmanaged with-overridden-fork-point" -f -a slidable-after             -d 'the downstream branch of the <branch>, if it exists and is the only downstream of <branch> (i.e. the one that can be slid out immediately following <branch>)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from list; and __fish_seen_subcommand_from slidable-after" -f -a '(__fish_git_local_branches)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from addable managed slidable slidable-after unmanaged with-overridden-fork-point" -f -a unmanaged                  -d 'all local branches that don\'t appear in the definition file'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from addable managed slidable slidable-after unmanaged with-overridden-fork-point" -f -a with-overridden-fork-point -d 'all local branches that have a fork point override set up (even if this override does not affect the location of their fork point anymore).'
+
+# git machete log
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a log -d 'Log the part of history specific to the given branch'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from log" -f -a '(__fish_git_local_branches)'
+
+# git machete reapply
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a reapply -d 'Rebase the current branch onto its computed fork point'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from reapply" -x -l fork-point -s f -a '(__fish_git_local_branches)' -d 'Specifies the alternative fork point commit after which the rebased part of history is meant to start'
+
+# git machete show
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a show -d 'Show name(s) of the branch(es) relative to the position of a branch, accepts down/first/last/next/root/prev/up argument'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from show; and not __fish_seen_subcommand_from current c down d first f last l next n prev p root r up u" -f -a 'current c' -d 'the current branch; exits with a non-zero status if none (detached HEAD)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from show; and not __fish_seen_subcommand_from current c down d first f last l next n prev p root r up u" -f -a 'down d'    -d 'the direct children/downstream branch of the given branch'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from show; and not __fish_seen_subcommand_from current c down d first f last l next n prev p root r up u" -f -a 'first f'   -d 'the first downstream of the root branch of the given branch (like root followed by next), or the root branch itself if the root has no downstream branches'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from show; and not __fish_seen_subcommand_from current c down d first f last l next n prev p root r up u" -f -a 'last l'    -d 'the last branch in the definition file that has the same root as the given branch; can be the root branch itself if the root has no downstream branches'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from show; and not __fish_seen_subcommand_from current c down d first f last l next n prev p root r up u" -f -a 'next n'    -d 'the direct successor of the given branch in the definition file'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from show; and not __fish_seen_subcommand_from current c down d first f last l next n prev p root r up u" -f -a 'prev p'    -d 'the direct predecessor of the given branch in the definition file'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from show; and not __fish_seen_subcommand_from current c down d first f last l next n prev p root r up u" -f -a 'root r'    -d 'the root of the tree where the given branch is located'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from show; and not __fish_seen_subcommand_from current c down d first f last l next n prev p root r up u" -f -a 'up u'      -d 'the direct parent/upstream branch of the given branch'
+
+# 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 -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'
+
+# git machete squash
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a squash -d 'Squash the unique history of the current branch into a single commit'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from squash" -x -l fork-point -s f -a '(__fish_git_local_branches)' -d 'Specifies the alternative fork point commit after which the squashed part of history is meant to start'
+
+# git machete status
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a status -d 'Display formatted tree of branch dependencies, including info on their sync with upstream branch and with remote'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from status s; and not __fish_seen_subcommand_from --color"                       -f -l color                    -a "auto always never" -d 'Colorize the output (default: auto)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from status s; and not __fish_seen_subcommand_from --list-commits -l"             -f -l list-commits             -s l                   -d 'Additionally list the commits introduced on each branch'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from status s; and not __fish_seen_subcommand_from --list-commits-with-hashes -L" -f -l list-commits-with-hashes -s L                   -d 'Additionally list the short hashes and messages of commits introduced on each branch'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from status s; and not __fish_seen_subcommand_from --no-detect-squash-merges"     -f -l no-detect-squash-merges                         -d 'Only consider "strict" (fast-forward or 2-parent) merges, rather than rebase/squash merges, when detecting if a branch is merged into its upstream (parent)'
+
+# git machete traverse
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a traverse -d 'Walk through the tree of branch dependencies and rebase, merge, slide out, push and/or pull each branch one by one'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --fetch -F"                -f -l fetch        -s F -d 'Fetch the remotes of all managed branches at the beginning of traversal (no git pull involved, only git fetch)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --list-commits -l"         -f -l list-commits -s l -d 'When printing the status, additionally list the messages of commits introduced on each branch'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --merge -M"                -f -l merge        -s M -d 'Update by merge rather than by rebase'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --yes -y"                  -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 traverse; and not __fish_seen_subcommand_from --no-detect-squash-merges" -f -l no-detect-squash-merges -d 'Only consider "strict" (fast-forward or 2-parent) merges, rather than rebase/squash merges, when detecting if a branch is merged into its upstream (parent)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --no-edit-merge; 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 traverse; and not __fish_seen_subcommand_from --no-interactive-rebase; and not __fish_seen_subcommand_from --merge -M" -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'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --no-push"           -f -l no-push           -d 'Do not push any (neither tracked nor untracked) branches to remote, re-enable via --push'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --no-push-untracked" -f -l no-push-untracked -d 'Do not push untracked branches to remote, re-enable via --push-untracked'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --push"              -f -l push              -d 'Push all (both tracked and untracked) branches to remote - default behavior'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --push-untracked"    -f -l push-untracked    -d 'Push untracked branches to remote - default behavior'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --return-to"         -x -l return-to  -a 'stay here nearest-remaining' -d 'Specifies the branch to return after traversal is successfully completed; WHERE can be here (the current branch at the moment when traversal starts), nearest-remaining (nearest remaining branch in case the here branch has been slid out by the traversal) or stay (the default - just stay wherever the traversal stops). Note: when user quits by q/yq or when traversal is stopped because one of git actions fails, the behavior is always stay'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --start-from"        -x -l start-from -a 'here root first-root'        -d 'Specifies the branch to start the traversal from; WHERE can be here (the default - current branch, must be managed by git-machete), root (root branch of the current branch, as in git machete show root) or first-root (first listed managed branch)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --whole -w"          -f -l whole      -s w                             -d 'Equivalent to -n --start-from=first-root --return-to=nearest-remaining; useful for quickly traversing & syncing all branches (rather than doing more fine-grained operations on the local section of the branch tree)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from -W"                  -f               -s W                             -d 'Equivalent to --fetch --whole; useful for even more automated traversal of all branches'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from traverse; and not __fish_seen_subcommand_from --yes -y"            -f -l yes        -s y                             -d 'Don\'t ask for any interactive input, including confirmation of rebase/push/pull. Implies -n'
+
+# git machete update
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a update -d 'Sync the current branch with its upstream (parent) branch via rebase or merge'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from update; and not __fish_seen_subcommand_from --merge -M"                                            -x -l fork-point -s f -a '(__fish_git_local_branches)' -d 'If updating by rebase, specifies the alternative fork point commit after which the rebased part of history is meant to start. Not allowed if updating by merge'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from update; 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 update; and not __fish_seen_subcommand_from -n --no-edit-merge --no-interactive-rebase"            -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)'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from update; 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'
+complete -c git -n "__fish_git_using_command machete; and __fish_seen_subcommand_from update; and not __fish_seen_subcommand_from --merge -M"                                            -f -l merge      -s M                                  -d 'Update by merge rather than by rebase'
+
+# git machete version
+complete -c git -n "__fish_git_using_command machete; and not __fish_seen_subcommand_from $__mcht_commands" -f -a version -d 'Display the version and exit'
diff --git a/debian/source/options b/debian/source/options
index b3e9f73..a84c771 100644
--- a/debian/source/options
+++ b/debian/source/options
@@ -1,4 +1,5 @@
 tar-ignore = AUTHORS
+tar-ignore = blogs
 tar-ignore = build
 tar-ignore = ChangeLog
 tar-ignore = ci
@@ -11,6 +12,7 @@ tar-ignore = docs
 tar-ignore = *.egg-info
 tar-ignore = .eggs
 tar-ignore = *.envsubst
+tar-ignore = graphics
 tar-ignore = .git
 tar-ignore = .gitignore
 tar-ignore = hook_samples
@@ -18,8 +20,8 @@ tar-ignore = htmlcov
 tar-ignore = .idea
 tar-ignore = ISSUE_TEMPLATE.md
 tar-ignore = LICENSE
-tar-ignore = logo.png
 tar-ignore = Makefile
+tar-ignore = mypy.ini
 tar-ignore = __pycache__
 tar-ignore = *.pyc
 tar-ignore = .python-version
@@ -30,4 +32,4 @@ tar-ignore = .stestr.conf
 tar-ignore = *.swp
 tar-ignore = tests
 tar-ignore = .tox
-tar-ignore = tox.ini
+tar-ignore = tox.ini
\ No newline at end of file
diff --git a/docs/source/cli_help/github.rst b/docs/source/cli_help/github.rst
index b7745eb..759e63b 100644
--- a/docs/source/cli_help/github.rst
+++ b/docs/source/cli_help/github.rst
@@ -16,8 +16,9 @@ To allow GitHub API access for private repositories (and also to perform side-ef
 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.
 
 ``anno-prs``:
 
diff --git a/docs/source/cli_help/hooks.rst b/docs/source/cli_help/hooks.rst
index d773d7c..d2bcbb0 100644
--- a/docs/source/cli_help/hooks.rst
+++ b/docs/source/cli_help/hooks.rst
@@ -25,7 +25,7 @@ Note: ``hooks`` is not a command as such, just a help topic (there is no ``git m
         * exactly once during a ``slide-out`` execution (even if multiple branches are slid out),
         * zero or more times during ``traverse`` (every time a slide-out operation is confirmed).
 
-    If the hook returns a non-zero exit code, then the execution of the command is aborted,
+    If the hook returns a non-zero exit code, then an error is raised and the execution of the command is aborted,
     i.e. ``slide-out`` won't attempt rebase of the new downstream branches and ``traverse`` won't continue the traversal.
     In case of ``advance`` there is no difference (other than exit code of the entire ``advance`` command being non-zero),
     since slide-out is the last operation that happens within ``advance``.
@@ -44,7 +44,7 @@ Note: ``hooks`` is not a command as such, just a help topic (there is no ``git m
         2. what is the fork point --- the place where the rebased history diverges from the upstream history,
         3. what branch is rebased.
 
-    If the hook returns a non-zero exit code, the entire rebase is aborted.
+    If the hook returns a non-zero exit code, an error is raised and the entire rebase is aborted.
 
     Note: this hook is independent from git's standard ``pre-rebase hook``.
     If machete-pre-rebase returns zero, the execution flow continues to ``git rebase``, which may also run ``pre-rebase hook`` if present.
diff --git a/docs/source/cli_help/traverse.rst b/docs/source/cli_help/traverse.rst
index 382f6b9..8b23310 100644
--- a/docs/source/cli_help/traverse.rst
+++ b/docs/source/cli_help/traverse.rst
@@ -18,7 +18,9 @@ traverse
                          [--[no-]push] [--[no-]push-untracked]
                          [--return-to=WHERE] [--start-from=WHERE] [-w|--whole] [-W] [-y|--yes]
 
-Traverses the branch dependency tree in pre-order (i.e. simply in the order as they occur in the definition file) and for each branch:
+Traverses the branch dependency tree in pre-order (i.e. simply in the order as they occur in the definition file).
+By default traverse starts from current branch, however this behaviour can be customized using options: ``--start-from=``, ``--whole`` or ``-w``, ``-W``.
+For each branch, the command:
 
     * detects if the branch is merged to its parent/upstream:
 
diff --git a/docs/source/completion.rst b/docs/source/completion.rst
index 2fda521..2c01e6a 100644
--- a/docs/source/completion.rst
+++ b/docs/source/completion.rst
@@ -94,3 +94,17 @@ Zsh:
             .. code-block:: shell
 
                 exec $SHELL -l
+
+Fish:
+
+    * Mac (via Homebrew)
+        Please look at the section about [installation via Homebrew](https://github.com/VirtusLab/git-machete#using-homebrew-macos).
+        ``brew install git-machete`` automatically installs fish completion files for ``git machete``.
+    * Linux
+        #. Place the completion script in ``/path/to/fish/completions/`` (typically ``~/.config/fish/completions/git-machete.fish``).
+
+        .. code-block:: shell
+
+            mkdir -p ~/.config/fish/completions
+            curl -L https://raw.githubusercontent.com/VirtusLab/git-machete/develop/completion/git-machete.fish -o ~/.config/fish/completions/git-machete.fish
+            echo "source ~/.config/fish/completions/git-machete.fish >/dev/null" >> ~/.config/fish/config.fish
diff --git a/git-machete b/git-machete
index 0b871fc..c0b408d 100644
--- a/git-machete
+++ b/git-machete
@@ -1,4 +1,19 @@
 #!/usr/bin/env python3
 
+import sys
+
+# Check for correct python version
+# Since function below needs to be compatible with python 2, lets skip Mypy checks, cause type annotations were introduced in python 3.5
+def validate_python_version():  # type: ignore
+  if sys.version_info[:2] < (3, 6):
+    version_str = "{}.{}.{}".format(sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
+    sys.stderr.write(
+      "Python {} is no longer supported. \n".format(version_str) +
+      "Please switch to Python 3.6 or higher.\n")
+    sys.exit(1)
+
+
+validate_python_version()  # type: ignore
+
 from git_machete import cli
 cli.main()
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index da4564d..01bd03c 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.4.1'
+__version__ = '3.5.0'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index e80af3f..8824ec9 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -14,7 +14,7 @@ from git_machete.client import MacheteClient
 from git_machete.constants import EscapeCodes
 from git_machete.docs import short_docs, long_docs
 from git_machete.exceptions import MacheteException, StopInteraction
-from git_machete.git_operations import GitContext
+from git_machete.git_operations import AnyRevision, GitContext, LocalBranchShortName, RemoteBranchShortName
 from git_machete.utils import fmt, underline, excluding, warn
 
 T = TypeVar('T')
@@ -30,17 +30,18 @@ alias_by_command: Dict[str, str] = {
 }
 
 command_groups: List[Tuple[str, List[str]]] = [
-    # 'is-managed' is mostly for scripting use and therefore skipped
     ("General topics",
-     ["file", "help", "hooks", "version"]),
+     ["file", "format", "help", "hooks", "version"]),
     ("Build, display and modify the tree of branch dependencies",
      ["add", "anno", "discover", "edit", "status"]),
     ("List, check out and delete branches",
-     ["delete-unmanaged", "go", "list", "show"]),
+     ["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",
-     ["advance", "reapply", "slide-out", "squash", "traverse", "update"])
+     ["advance", "reapply", "slide-out", "squash", "traverse", "update"]),
+    ("Integrate with third party tools",
+     ["github"])
 ]
 
 
@@ -109,7 +110,7 @@ class MacheteHelpAction(argparse.Action):
         # parser name (prog) is expected to be `git machete` or `git machete <command>`
         command_name = parser.prog.replace('git machete', '').strip()
         print(get_help_description(command_name))
-        parser.exit()
+        parser.exit(status=None)
 
 
 def create_cli_parser() -> argparse.ArgumentParser:
@@ -357,15 +358,25 @@ def create_cli_parser() -> argparse.ArgumentParser:
 def update_cli_opts_using_parsed_args(
         cli_opts: git_machete.options.CommandLineOptions,
         parsed_args: argparse.Namespace) -> None:
+
+    # Warning: In mypy, Arguments that come from untyped functions/variables are silently treated by mypy as Any.
+    # Since argparse is not typed, everything that comes from argparse.Namespace will be taken as Any :(
+    # Even if we add type=LocalBranchShortName into argument parser for branch,
+    # python debugger will see branch as LocalBranchShortName but mypy always will see it as Any,
+    # until you specifically tell mypy what is the exact type by casting (right now it's done this way below, but casting does not solve all of the problems).
+    #
+    # The reasonable solution here would be to use Typed Argument Parser which is a wrapper over argparse with modernised solution for typing.
+    # But it would add external dependency to git-machete, so let's stick to current casting.
+
     for opt, arg in vars(parsed_args).items():
         if opt == "branch":
-            cli_opts.opt_branch = arg
+            cli_opts.opt_branch = LocalBranchShortName.of(arg)
         elif opt == "checked_out_since":
             cli_opts.opt_checked_out_since = arg
         elif opt == "color":
             cli_opts.opt_color = arg
         elif opt == "down_fork_point":
-            cli_opts.opt_down_fork_point = arg
+            cli_opts.opt_down_fork_point = AnyRevision.of(arg)
         elif opt == "debug":
             cli_opts.opt_debug = True
         elif opt == "draft":
@@ -373,7 +384,7 @@ def update_cli_opts_using_parsed_args(
         elif opt == "fetch":
             cli_opts.opt_fetch = True
         elif opt == "fork_point":
-            cli_opts.opt_fork_point = arg
+            cli_opts.opt_fork_point = AnyRevision.of(arg)
         elif opt == "inferred":
             cli_opts.opt_inferred = True
         elif opt == "list_commits_with_hashes":
@@ -396,7 +407,7 @@ def update_cli_opts_using_parsed_args(
         elif opt == "no_push_untracked":
             cli_opts.opt_push_untracked = False
         elif opt == "onto":
-            cli_opts.opt_onto = arg
+            cli_opts.opt_onto = LocalBranchShortName.of(arg)
         elif opt == "override_to":
             cli_opts.opt_override_to = arg
         elif opt == "override_to_inferred":
@@ -411,7 +422,7 @@ def update_cli_opts_using_parsed_args(
         elif opt == "as_root":
             cli_opts.opt_as_root = True
         elif opt == "roots":
-            cli_opts.opt_roots = arg.split(",")
+            cli_opts.opt_roots = list(map(LocalBranchShortName.of, arg.split(",")))
         elif opt == "return_to":
             cli_opts.opt_return_to = arg
         elif opt == "stat":
@@ -454,31 +465,31 @@ def set_utils_global_variables(
     utils.verbose_mode = cli_opts.opt_verbose
 
 
-def validate_python_version() -> None:
-    if sys.version_info[:2] < (3, 6):
-        version_str = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
-        sys.stderr.write(
-            f"Python {version_str} is no longer supported. "
-            "Please switch to Python 3.6 or higher.\n")
-        sys.exit(1)
-
-
 def get_branch_arg_or_current_branch(
-        cli_opts: git_machete.options.CommandLineOptions, git_context: GitContext) -> str:
+        cli_opts: git_machete.options.CommandLineOptions, git_context: GitContext) -> LocalBranchShortName:
     return cli_opts.opt_branch or git_context.get_current_branch()
 
 
+def exit_script(status_code: Optional[int] = None) -> None:
+    # Single point of exit is useful, because we can mock this method in tests
+    # and verify that actual errors like MacheteException are raised with
+    # appropriate messages. Otherwise it's not possible, because SystemError
+    # caused by sys.exit() is overshadowing actual errors. As of the issue #287,
+    # there are some additional points of exit from the script in client.py
+    # method MacheteClient.pick() there are left on purpose since removing them
+    # would enforce significant refactor.
+    sys.exit(status_code)
+
+
 def launch(orig_args: List[str]) -> None:
     if not orig_args:
         print(get_help_description())
-        sys.exit(2)
+        exit_script(2)
 
     cli_opts = git_machete.options.CommandLineOptions()
-    git = GitContext(cli_opts)
+    git = GitContext()
 
     try:
-        machete_client = MacheteClient(cli_opts, git)
-
         cli_parser: argparse.ArgumentParser = create_cli_parser()
         parsed_cli: argparse.Namespace = cli_parser.parse_args(orig_args)
         parsed_cli_as_dict: Dict[str, str] = vars(parsed_cli)
@@ -489,7 +500,13 @@ def launch(orig_args: List[str]) -> None:
 
         cmd = parsed_cli.command
 
-        if cmd not in {'help', 'discover'}:
+        if cmd == "help":
+            print(get_help_description(parsed_cli.topic_or_cmd))
+            exit_script()
+
+        machete_client = MacheteClient(git)
+
+        if cmd != 'discover':
             if not os.path.exists(machete_client.definition_file_path):
                 # We're opening in "append" and not "write" mode to avoid a race condition:
                 # if other process writes to the file between we check the
@@ -508,13 +525,17 @@ def launch(orig_args: List[str]) -> None:
         if cmd == "add":
             machete_client.read_definition_file()
             branch = get_branch_arg_or_current_branch(cli_opts, git)
-            machete_client.add(branch)
+            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)
         elif cmd == "advance":
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
             machete_client.expect_in_managed_branches(current_branch)
-            machete_client.advance(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)
             if cli_opts.opt_sync_github_prs:
@@ -528,13 +549,17 @@ def launch(orig_args: List[str]) -> None:
                     machete_client.print_annotation(branch)
         elif cmd == "delete-unmanaged":
             machete_client.read_definition_file()
-            machete_client.delete_unmanaged()
+            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.diff(parsed_cli.branch)  # passing None if not specified
+            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.
-            machete_client.discover_tree()
+            machete_client.discover_tree(
+                opt_checked_out_since=cli_opts.opt_checked_out_since,
+                opt_list_commits=cli_opts.opt_list_commits,
+                opt_roots=cli_opts.opt_roots,
+                opt_yes=cli_opts.opt_yes)
         elif cmd in {"edit", alias_by_command["edit"]}:
             # No need to read definition file.
             machete_client.edit()
@@ -545,12 +570,18 @@ def launch(orig_args: List[str]) -> None:
             machete_client.read_definition_file()
             branch = get_branch_arg_or_current_branch(cli_opts, git)
             if cli_opts.opt_inferred:
-                print(machete_client.fork_point(branch, use_overrides=False))
+                print(machete_client.fork_point(
+                    branch=branch,
+                    use_overrides=False,
+                    opt_no_detect_squash_merges=cli_opts.opt_no_detect_squash_merges))
             elif cli_opts.opt_override_to:
-                machete_client.set_fork_point_override(branch, cli_opts.opt_override_to)
+                machete_client.set_fork_point_override(branch, AnyRevision.of(cli_opts.opt_override_to))
             elif cli_opts.opt_override_to_inferred:
                 machete_client.set_fork_point_override(
-                    branch, machete_client.fork_point(branch, use_overrides=False))
+                    branch, machete_client.fork_point(
+                        branch=branch,
+                        use_overrides=False,
+                        opt_no_detect_squash_merges=cli_opts.opt_no_detect_squash_merges))
             elif cli_opts.opt_override_to_parent:
                 upstream = machete_client.up_branch.get(branch)
                 if upstream:
@@ -561,13 +592,16 @@ def launch(orig_args: List[str]) -> None:
             elif cli_opts.opt_unset_override:
                 machete_client.unset_fork_point_override(branch)
             else:
-                print(machete_client.fork_point(branch, use_overrides=True))
+                print(machete_client.fork_point(
+                    branch=branch,
+                    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()
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
             dest = machete_client.parse_direction(
-                parsed_cli.direction, current_branch, allow_current=False, down_pick_mode=True)
+                parsed_cli.direction, current_branch, allow_current=False, down_pick_mode=True)[0]  # with down_pick_mode=True there is only one element in list allowed
             if dest != current_branch:
                 git.checkout(dest)
         elif cmd == "github":
@@ -595,19 +629,19 @@ def launch(orig_args: List[str]) -> None:
                 machete_client.checkout_github_prs(pr_no)
             elif github_subcommand == "create-pr":
                 current_branch = git.get_current_branch()
-                machete_client.create_github_pr(current_branch, draft=cli_opts.opt_draft)
+                machete_client.create_github_pr(
+                    head=current_branch,
+                    opt_draft=cli_opts.opt_draft,
+                    opt_onto=cli_opts.opt_onto)
             elif github_subcommand == "retarget-pr":
                 current_branch = git.get_current_branch()
                 machete_client.expect_in_managed_branches(current_branch)
                 machete_client.retarget_github_pr(current_branch)
-        elif cmd == "help":
-            # No need to read definition file.
-            print(get_help_description(parsed_cli.topic_or_cmd))
         elif cmd == "is-managed":
             machete_client.read_definition_file()
             branch = get_branch_arg_or_current_branch(cli_opts, git)
             if branch is None or branch not in machete_client.managed_branches:
-                sys.exit(1)
+                exit_script(1)
         elif cmd == "list":
             category = parsed_cli.category
             if category == 'slidable-after' and 'branch' not in parsed_cli_as_dict:
@@ -620,23 +654,23 @@ def launch(orig_args: List[str]) -> None:
             machete_client.read_definition_file()
             res = []
             if category == "addable":
-                def strip_first_fragment(remote_branch: str) -> str:
-                    return re.sub("^[^/]+/", "", remote_branch)
+                def strip_remote_name(remote_branch: RemoteBranchShortName) -> LocalBranchShortName:
+                    return LocalBranchShortName.of(re.sub("^[^/]+/", "", remote_branch))
 
                 remote_counterparts_of_local_branches = utils.map_truthy_only(
-                    lambda _branch: git.get_combined_counterpart_for_fetching_of_branch(_branch),
+                    lambda _branch: LocalBranchShortName.of(git.get_combined_counterpart_for_fetching_of_branch(_branch)),
                     git.get_local_branches())
-                qualifying_remote_branches = excluding(git.get_remote_branches(),
-                                                       remote_counterparts_of_local_branches)
+                qualifying_remote_branches: List[RemoteBranchShortName] = excluding(git.get_remote_branches(),
+                                                                                    {RemoteBranchShortName.of(b) for b in remote_counterparts_of_local_branches})
                 res = excluding(git.get_local_branches(), machete_client.managed_branches) + list(
-                    map(strip_first_fragment, qualifying_remote_branches))
+                    map(strip_remote_name, qualifying_remote_branches))
             elif category == "managed":
                 res = machete_client.managed_branches
             elif category == "slidable":
-                res = machete_client.slidable()
+                res = machete_client.get_slidable_branches()
             elif category == "slidable-after":
                 machete_client.expect_in_managed_branches(parsed_cli.branch)
-                res = machete_client.slidable_after(parsed_cli.branch)
+                res = machete_client.get_slidable_after(parsed_cli.branch)
             elif category == "unmanaged":
                 res = excluding(git.get_local_branches(), machete_client.managed_branches)
             elif category == "with-overridden-fork-point":
@@ -655,42 +689,65 @@ def launch(orig_args: List[str]) -> None:
             machete_client.read_definition_file()
             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)
             git.rebase_onto_ancestor_commit(
                 current_branch,
                 cli_opts.opt_fork_point or machete_client.fork_point(
-                    current_branch, use_overrides=True))
+                    branch=current_branch,
+                    use_overrides=True,
+                    opt_no_detect_squash_merges=cli_opts.opt_no_detect_squash_merges),
+                cli_opts.opt_no_interactive_rebase)
         elif cmd == "show":
             direction = parsed_cli.direction
-            if direction == "current" and 'branch' in parsed_cli:
-                raise MacheteException(
-                    '`show current` with a <branch> argument does not make sense')
-            machete_client.read_definition_file(verify_branches=False)
             branch = get_branch_arg_or_current_branch(cli_opts, git)
-            print(
-                machete_client.parse_direction(
-                    direction,
-                    branch,
-                    allow_current=True,
-                    down_pick_mode=False
+            if direction == "current":
+                if 'branch' in parsed_cli:
+                    raise MacheteException(
+                        '`show current` with a <branch> argument does not make sense')
+                print(branch)
+            else:
+                machete_client.read_definition_file(verify_branches=False)
+                print(
+                    '\n'.join(machete_client.parse_direction(
+                        direction,
+                        branch,
+                        allow_current=True,
+                        down_pick_mode=False
+                    ))
                 )
-            )
         elif cmd == "slide-out":
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
             branches = parsed_cli_as_dict.get('branches', [git.get_current_branch()])
-            machete_client.slide_out(list(branches))
+            machete_client.slide_out(
+                branches_to_slide_out=list(map(LocalBranchShortName.of, branches)),
+                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()
             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.squash(
-                current_branch,
-                cli_opts.opt_fork_point or machete_client.fork_point(
-                    current_branch, use_overrides=True))
+                current_branch=current_branch,
+                opt_fork_point=cli_opts.opt_fork_point or machete_client.fork_point(
+                    branch=current_branch,
+                    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.expect_at_least_one_managed_branch()
-            machete_client.status(warn_on_yellow_edges=True)
+            machete_client.status(
+                warn_on_yellow_edges=True,
+                opt_list_commits=cli_opts.opt_list_commits,
+                opt_list_commits_with_hashes=cli_opts.opt_list_commits_with_hashes,
+                opt_no_detect_squash_merges=cli_opts.opt_no_detect_squash_merges)
         elif cmd == "traverse":
             if cli_opts.opt_start_from not in {"here", "root", "first-root"}:
                 raise MacheteException(
@@ -702,25 +759,43 @@ def launch(orig_args: List[str]) -> None:
                     "Valid arguments: `here|nearest-remaining|stay`.")
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
-            machete_client.traverse()
+            machete_client.traverse(
+                opt_fetch=cli_opts.opt_fetch,
+                opt_list_commits=cli_opts.opt_list_commits,
+                opt_merge=cli_opts.opt_merge,
+                opt_no_detect_squash_merges=cli_opts.opt_no_detect_squash_merges,
+                opt_no_edit_merge=cli_opts.opt_no_edit_merge,
+                opt_no_interactive_rebase=cli_opts.opt_no_interactive_rebase,
+                opt_push_tracked=cli_opts.opt_push_tracked,
+                opt_push_untracked=cli_opts.opt_push_untracked,
+                opt_return_to=cli_opts.opt_return_to,
+                opt_start_from=cli_opts.opt_start_from,
+                opt_yes=cli_opts.opt_yes)
         elif cmd == "update":
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
-            machete_client.update()
+            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.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)
         elif cmd == "version":
             version()
-            sys.exit()
+            exit_script()
 
     except (argparse.ArgumentError, argparse.ArgumentTypeError) as e:
         print(get_short_general_usage())
         sys.stderr.write(f"\n{e}\n")
-        sys.exit(2)
+        exit_script(2)
     except MacheteException as e:
         sys.stderr.write(f"\n{e}\n")
-        sys.exit(1)
+        exit_script(1)
     except KeyboardInterrupt:
         sys.stderr.write("\nInterrupted by the user")
-        sys.exit(1)
+        exit_script(1)
     except StopInteraction:
         pass
     finally:
@@ -735,7 +810,6 @@ def launch(orig_args: List[str]) -> None:
 
 
 def main() -> None:
-    validate_python_version()
     launch(sys.argv[1:])
 
 
diff --git a/git_machete/client.py b/git_machete/client.py
index 1ce26ab..0b32f19 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -4,17 +4,16 @@ import itertools
 import os
 import shutil
 import sys
-from typing import List, Dict, Optional, Tuple, Generator, Callable
+from typing import Callable, Dict, Generator, List, Optional, Tuple
 
 import git_machete.github
 import git_machete.options
 from git_machete import utils
 from git_machete.constants import (
-    DISCOVER_DEFAULT_FRESH_BRANCH_COUNT, PICK_FIRST_ROOT, UNTRACKED,
-    AHEAD_OF_REMOTE, BEHIND_REMOTE, DIVERGED_FROM_AND_OLDER_THAN_REMOTE,
-    DIVERGED_FROM_AND_NEWER_THAN_REMOTE, NO_REMOTES, IN_SYNC_WITH_REMOTE, PICK_LAST_ROOT, EscapeCodes)
+    DISCOVER_DEFAULT_FRESH_BRANCH_COUNT, PICK_FIRST_ROOT, PICK_LAST_ROOT,
+    EscapeCodes, SyncToRemoteStatuses)
 from git_machete.exceptions import MacheteException, StopInteraction
-from git_machete.git_operations import GitContext
+from git_machete.git_operations import AnyBranchName, AnyRevision, FullCommitHash, GitContext, HEAD, LocalBranchShortName, RemoteBranchShortName
 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,
@@ -25,7 +24,7 @@ from git_machete.utils import (
     colored, underline, dim, get_second)
 
 
-BRANCH_DEF = Tuple[str, str]
+BranchDef = Tuple[LocalBranchShortName, str]
 Hash_ShortHash_Message = Tuple[str, str, str]
 
 
@@ -37,41 +36,40 @@ def allowed_directions(allow_current: bool) -> str:
 
 class MacheteClient:
 
-    def __init__(self, cli_opts: git_machete.options.CommandLineOptions, git: GitContext) -> None:
-        self.__cli_opts: git_machete.options.CommandLineOptions = cli_opts
+    def __init__(self, git: GitContext) -> None:
         self.__git: GitContext = git
         self._definition_file_path: str = self.__git.get_git_subpath("machete")
-        self._managed_branches: List[str] = []
-        self._up_branch: Dict[str, str] = {}  # TODO (#110): default dict with None
-        self.__down_branches: Dict[str, List[str]] = {}  # TODO (#110): default dict with []
+        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 []
         self.__indent: Optional[str] = None
-        self.__roots: List[str] = []
-        self.__annotations: Dict[str, str] = {}
+        self.__roots: List[LocalBranchShortName] = []
+        self.__annotations: Dict[LocalBranchShortName, str] = {}
         self.__empty_line_status: Optional[bool] = None
         self.__branch_defs_by_sha_in_reflog: Optional[
-            Dict[str, Optional[List[Tuple[str, str]]]]] = None
+            Dict[FullCommitHash, Optional[List[Tuple[LocalBranchShortName, str]]]]] = None
 
     @property
     def definition_file_path(self) -> str:
         return self._definition_file_path
 
     @property
-    def managed_branches(self) -> List[str]:
+    def managed_branches(self) -> List[LocalBranchShortName]:
         return self._managed_branches
 
     @managed_branches.setter
-    def managed_branches(self, val: List[str]) -> None:
+    def managed_branches(self, val: List[LocalBranchShortName]) -> None:
         self._managed_branches = val
 
     @property
-    def up_branch(self) -> Dict[str, str]:
+    def up_branch(self) -> Dict[LocalBranchShortName, Optional[LocalBranchShortName]]:
         return self._up_branch
 
     @up_branch.setter
-    def up_branch(self, val: Dict[str, str]) -> None:
+    def up_branch(self, val: Dict[LocalBranchShortName, Optional[LocalBranchShortName]]) -> None:
         self._up_branch = val
 
-    def expect_in_managed_branches(self, branch: str) -> None:
+    def expect_in_managed_branches(self, branch: LocalBranchShortName) -> None:
         if branch not in self.managed_branches:
             raise MacheteException(
                 f"Branch `{branch}` not found in the tree of branch dependencies."
@@ -96,7 +94,7 @@ class MacheteClient:
 
         hint = "Edit the definition file manually with `git machete edit`"
 
-        invalid_branches: List[str] = []
+        invalid_branches: List[LocalBranchShortName] = []
         for index, line in enumerate(lines):
             if line == "" or line.isspace():
                 continue
@@ -104,10 +102,10 @@ class MacheteClient:
             if prefix and not self.__indent:
                 self.__indent = prefix
 
-            b_a: List[str] = line.strip().split(" ", 1)
-            branch = b_a[0]
-            if len(b_a) > 1:
-                self.__annotations[branch] = b_a[1]
+            branch_and_maybe_annotation: List[LocalBranchShortName] = [LocalBranchShortName.of(entry) for entry in line.strip().split(" ", 1)]
+            branch = branch_and_maybe_annotation[0]
+            if len(branch_and_maybe_annotation) > 1:
+                self.__annotations[branch] = branch_and_maybe_annotation[1]
             if branch in self.managed_branches:
                 raise MacheteException(
                     f"{self._definition_file_path}, line {index + 1}: branch "
@@ -155,33 +153,33 @@ class MacheteClient:
                 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)
+                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_msg=None, opt_yes=False)
 
-        def recursive_slide_out_invalid_branches(branch: str) -> List[str]:
+        def recursive_slide_out_invalid_branches(branch_: LocalBranchShortName) -> List[LocalBranchShortName]:
             new_down_branches = flat_map(
-                recursive_slide_out_invalid_branches, self.__down_branches.get(branch, []))
-            if branch in invalid_branches:
-                if branch in self.__down_branches:
-                    del self.__down_branches[branch]
-                if branch in self.__annotations:
-                    del self.__annotations[branch]
-                if branch in self.up_branch:
+                recursive_slide_out_invalid_branches, self.__down_branches.get(branch_, []))
+            if branch_ in invalid_branches:
+                if branch_ in self.__down_branches:
+                    del self.__down_branches[branch_]
+                if branch_ in self.__annotations:
+                    del self.__annotations[branch_]
+                if branch_ in self.up_branch:
                     for down_branch in new_down_branches:
-                        self.up_branch[down_branch] = self.up_branch[branch]
-                    del self.up_branch[branch]
+                        self.up_branch[down_branch] = self.up_branch[branch_]
+                    del self.up_branch[branch_]
                 else:
                     for down_branch in new_down_branches:
                         del self.up_branch[down_branch]
                 return new_down_branches
             else:
-                self.__down_branches[branch] = new_down_branches
-                return [branch]
+                self.__down_branches[branch_] = new_down_branches
+                return [branch_]
 
         self.__roots = flat_map(recursive_slide_out_invalid_branches, self.__roots)
         self.managed_branches = excluding(self.managed_branches, invalid_branches)
@@ -195,7 +193,7 @@ class MacheteClient:
         if not self.__indent:
             self.__indent = "\t"
 
-        def render_dfs(branch: str, depth: int) -> List[str]:
+        def render_dfs(branch: LocalBranchShortName, depth: int) -> List[str]:
             self.annotation = f" {self.__annotations[branch]}" if branch in self.__annotations else ""
             res: List[str] = [depth * self.__indent + branch + self.annotation]
             for down_branch in self.__down_branches.get(branch, []):
@@ -214,52 +212,51 @@ class MacheteClient:
         with open(self._definition_file_path, "w") as file:
             file.write("\n".join(self.render_tree()) + "\n")
 
-    def add(self, branch: str) -> None:
+    def add(self, *, branch: LocalBranchShortName, opt_onto: Optional[LocalBranchShortName], opt_as_root: bool, opt_yes: bool) -> None:
         if branch in self.managed_branches:
             raise MacheteException(
                 f"Branch `{branch}` already exists in the tree of branch dependencies")
 
-        onto: Optional[str] = self.__cli_opts.opt_onto
-        if onto:
-            self.expect_in_managed_branches(onto)
+        if opt_onto:
+            self.expect_in_managed_branches(opt_onto)
 
         if branch not in self.__git.get_local_branches():
-            remote_branch: Optional[str] = self.__git.get_sole_remote_branch(branch)
+            remote_branch: Optional[RemoteBranchShortName] = self.__git.get_sole_remote_branch(branch)
             if remote_branch:
                 common_line = (
                     f"A local branch `{branch}` does not exist, but a remote "
                     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) in ('y', 'yes'):
-                    self.__git.create_branch(branch, f"refs/remotes/{remote_branch}")
+                if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes) in ('y', 'yes'):
+                    self.__git.create_branch(branch, remote_branch.full_name())
                 else:
                     return
                 # Not dealing with `onto` here. If it hasn't been explicitly
                 # specified via `--onto`, we'll try to infer it now.
             else:
-                out_of = f"refs/heads/{onto}" if onto else "HEAD"
-                out_of_str = f"`{onto}`" if onto else "the current HEAD"
+                out_of = LocalBranchShortName.of(opt_onto).full_name() if opt_onto else HEAD
+                out_of_str = f"`{opt_onto}`" if opt_onto else "the current HEAD"
                 msg = (f"A local branch `{branch}` does not exist. Create (out "
                        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) in ('y', 'yes'):
+                if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes) 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 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:
-                            onto = current_branch
+                            opt_onto = current_branch
                     self.__git.create_branch(branch, out_of)
                 else:
                     return
 
-        if self.__cli_opts.opt_as_root or not self.__roots:
+        if opt_as_root or not self.__roots:
             self.__roots += [branch]
             print(fmt(f"Added branch `{branch}` as a new root"))
         else:
-            if not onto:
+            if not opt_onto:
                 upstream = self.__infer_upstream(
                     branch,
                     condition=lambda x: x in self.managed_branches,
@@ -276,22 +273,22 @@ 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) in ('y', 'yes'):
-                        onto = upstream
+                    if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes) in ('y', 'yes'):
+                        opt_onto = upstream
                     else:
                         return
 
-            self.up_branch[branch] = onto
-            if onto in self.__down_branches:
-                self.__down_branches[onto].append(branch)
+            self.up_branch[branch] = opt_onto
+            if opt_onto in self.__down_branches:
+                self.__down_branches[opt_onto].append(branch)
             else:
-                self.__down_branches[onto] = [branch]
-            print(fmt(f"Added branch `{branch}` onto `{onto}`"))
+                self.__down_branches[opt_onto] = [branch]
+            print(fmt(f"Added branch `{branch}` onto `{opt_onto}`"))
 
         self.managed_branches += [branch]
         self.save_definition_file()
 
-    def annotate(self, branch: str, words: List[str]) -> None:
+    def annotate(self, branch: LocalBranchShortName, words: List[str]) -> None:
 
         if branch in self.__annotations and words == ['']:
             del self.__annotations[branch]
@@ -299,13 +296,15 @@ class MacheteClient:
             self.__annotations[branch] = " ".join(words)
         self.save_definition_file()
 
-    def print_annotation(self, branch: str) -> None:
+    def print_annotation(self, branch: LocalBranchShortName) -> None:
         if branch in self.__annotations:
             print(self.__annotations[branch])
 
-    def update(self) -> None:
+    def update(
+            self, *, opt_merge: bool, opt_no_edit_merge: bool,
+            opt_no_interactive_rebase: bool, opt_fork_point: AnyRevision) -> None:
         current_branch = self.__git.get_current_branch()
-        if self.__cli_opts.opt_merge:
+        if opt_merge:
             with_branch = self.up(
                 current_branch,
                 prompt_if_inferred_msg=(
@@ -314,7 +313,7 @@ class MacheteClient:
                 prompt_if_inferred_yes_opt_msg=(
                     "Branch `%s` not found in the tree of branch dependencies. "
                     "Merging with the inferred upstream `%s`..."))
-            self.__git.merge(with_branch, current_branch)
+            self.__git.merge(with_branch, current_branch, opt_no_edit_merge)
         else:
             onto_branch = self.up(
                 current_branch,
@@ -325,28 +324,36 @@ class MacheteClient:
                     "Branch `%s` not found in the tree of branch dependencies. "
                     "Rebasing onto the inferred upstream `%s`..."))
             self.__git.rebase(
-                f"refs/heads/{onto_branch}",
-                self.__cli_opts.opt_fork_point or self.fork_point(current_branch, use_overrides=True),
-                current_branch)
-
-    def discover_tree(self) -> None:
+                LocalBranchShortName.of(onto_branch).full_name(),
+                opt_fork_point or self.fork_point(
+                    current_branch, use_overrides=True, opt_no_detect_squash_merges=False),
+                current_branch, opt_no_interactive_rebase)
+
+    def discover_tree(
+            self,
+            *,
+            opt_checked_out_since: Optional[str],
+            opt_list_commits: bool,
+            opt_roots: List[LocalBranchShortName],
+            opt_yes: bool
+    ) -> None:
         all_local_branches = self.__git.get_local_branches()
         if not all_local_branches:
             raise MacheteException("No local branches found")
-        for root in self.__cli_opts.opt_roots:
+        for root in opt_roots:
             if root not in self.__git.get_local_branches():
                 raise MacheteException(f"`{root}` is not a local branch")
-        if self.__cli_opts.opt_roots:
-            self.__roots = list(self.__cli_opts.opt_roots)
+        if opt_roots:
+            self.__roots = list(map(LocalBranchShortName.of, opt_roots))
         else:
             self.__roots = []
             if "master" in self.__git.get_local_branches():
-                self.__roots += ["master"]
+                self.__roots += [LocalBranchShortName.of("master")]
             elif "main" in self.__git.get_local_branches():
                 # See https://github.com/github/renaming
-                self.__roots += ["main"]
+                self.__roots += [LocalBranchShortName.of("main")]
             if "develop" in self.__git.get_local_branches():
-                self.__roots += ["develop"]
+                self.__roots += [LocalBranchShortName.of("develop")]
         self.__down_branches = {}
         self.up_branch = {}
         self.__indent = "\t"
@@ -354,7 +361,7 @@ class MacheteClient:
 
         root_of = dict((branch, branch) for branch in all_local_branches)
 
-        def get_root_of(branch: str) -> str:
+        def get_root_of(branch: LocalBranchShortName) -> LocalBranchShortName:
             if branch != root_of[branch]:
                 root_of[branch] = get_root_of(root_of[branch])
             return root_of[branch]
@@ -363,17 +370,17 @@ class MacheteClient:
         last_checkout_timestamps = self.__git.get_latest_checkout_timestamps()
         non_root_fixed_branches_by_last_checkout_timestamps = sorted(
             (last_checkout_timestamps.get(branch, 0), branch) for branch in non_root_fixed_branches)
-        if self.__cli_opts.opt_checked_out_since:
+        if opt_checked_out_since:
             threshold = self.__git.get_git_timespec_parsed_to_unix_timestamp(
-                self.__cli_opts.opt_checked_out_since)
-            stale_non_root_fixed_branches = [branch for (timestamp, branch) in itertools.takewhile(
+                opt_checked_out_since)
+            stale_non_root_fixed_branches = [LocalBranchShortName.of(branch) for (timestamp, branch) in itertools.takewhile(
                 tupled(lambda timestamp, branch: timestamp < threshold),
                 non_root_fixed_branches_by_last_checkout_timestamps
             )]
         else:
             c = DISCOVER_DEFAULT_FRESH_BRANCH_COUNT
             stale, fresh = non_root_fixed_branches_by_last_checkout_timestamps[:-c], non_root_fixed_branches_by_last_checkout_timestamps[-c:]
-            stale_non_root_fixed_branches = [branch for (timestamp, branch) in stale]
+            stale_non_root_fixed_branches = [LocalBranchShortName.of(branch) for (timestamp, branch) in stale]
             if stale:
                 threshold_date = datetime.datetime.utcfromtimestamp(fresh[0][0]).strftime("%Y-%m-%d")
                 warn(
@@ -384,7 +391,7 @@ class MacheteClient:
                     f"e.g. `'2 weeks ago'` or `2020-06-01`) to change this "
                     f"threshold so that less or more branches are included.\n")
         self.managed_branches = excluding(all_local_branches, stale_non_root_fixed_branches)
-        if self.__cli_opts.opt_checked_out_since and not self.managed_branches:
+        if opt_checked_out_since and not self.managed_branches:
             warn(
                 "no branches satisfying the criteria. Try moving the value of "
                 "`--checked-out-since` further to the past.")
@@ -422,7 +429,11 @@ class MacheteClient:
         for branch in self.managed_branches:
             if branch in self.up_branch and not self.__down_branches.get(branch):
                 upstream = self.up_branch[branch]
-                if self.is_merged_to(branch, upstream):
+                if self.is_merged_to(
+                        branch=branch,
+                        target=upstream,
+                        opt_no_detect_squash_merges=False
+                ):
                     debug(
                         "discover_tree()",
                         (f"inferred upstream of {branch} is {upstream}, but "
@@ -447,7 +458,11 @@ class MacheteClient:
             # This is rare enough, however, that we can pretty much ignore this corner case.
 
         print(bold("Discovered tree of branch dependencies:\n"))
-        self.status(warn_on_yellow_edges=False)
+        self.status(
+            warn_on_yellow_edges=False,
+            opt_list_commits=opt_list_commits,
+            opt_list_commits_with_hashes=False,
+            opt_no_detect_squash_merges=False)
         print("")
         do_backup = os.path.isfile(self._definition_file_path)
         backup_msg = (
@@ -455,7 +470,7 @@ class MacheteClient:
             if do_backup else "")
         msg = f"Save the above tree to {self._definition_file_path}?{backup_msg}" + get_pretty_choices('y', 'e[dit]', 'N')
         opt_yes_msg = f"Saving the above tree to {self._definition_file_path}... {backup_msg}"
-        ans = self.ask_if(msg, opt_yes_msg)
+        ans = self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes)
         if ans in ('y', 'yes'):
             if do_backup:
                 self.back_up_definition_file()
@@ -466,7 +481,15 @@ class MacheteClient:
             self.save_definition_file()
             self.edit()
 
-    def slide_out(self, branches_to_slide_out: List[str]) -> None:
+    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:
         # Verify that all branches exist, are managed, and have an upstream.
         for branch in branches_to_slide_out:
             self.expect_in_managed_branches(branch)
@@ -474,6 +497,20 @@ class MacheteClient:
             if not new_upstream:
                 raise MacheteException(f"No upstream branch defined for `{branch}`, cannot slide out")
 
+        if opt_down_fork_point:
+            last_branch_to_slide_out = branches_to_slide_out[-1]
+            children_of_the_last_branch_to_slide_out = self.__down_branches.get(
+                last_branch_to_slide_out)
+
+            if len(children_of_the_last_branch_to_slide_out) > 1:
+                raise MacheteException(
+                    "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,
+                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
         for bu, bd in zip(branches_to_slide_out[:-1], branches_to_slide_out[1:]):
@@ -514,26 +551,30 @@ class MacheteClient:
         self.__git.checkout(new_upstream)
         for new_downstream in new_downstreams:
             self.__git.checkout(new_downstream)
-            if self.__cli_opts.opt_merge:
+            if opt_merge:
                 print(f"Merging {bold(new_upstream)} into {bold(new_downstream)}...")
-                self.__git.merge(new_upstream, new_downstream)
+                self.__git.merge(new_upstream, new_downstream, opt_no_edit_merge)
             else:
                 print(f"Rebasing {bold(new_downstream)} onto {bold(new_upstream)}...")
                 self.__git.rebase(
-                    f"refs/heads/{new_upstream}",
-                    self.__cli_opts.opt_down_fork_point or
-                    self.fork_point(new_downstream, use_overrides=True), new_downstream)
-
-    def advance(self, branch: str) -> None:
+                    new_upstream.full_name(),
+                    opt_down_fork_point or
+                    self.fork_point(
+                        new_downstream, use_overrides=True, opt_no_detect_squash_merges=False),
+                    new_downstream,
+                    opt_no_interactive_rebase)
+
+    def advance(self, *, branch: LocalBranchShortName, opt_yes: bool) -> None:
         if not self.__down_branches.get(branch):
             raise MacheteException(
                 f"`{branch}` does not have any downstream (child) branches to advance towards")
 
-        def connected_with_green_edge(bd: str) -> bool:
+        def connected_with_green_edge(bd: LocalBranchShortName) -> bool:
             return bool(
-                not self.__is_merged_to_upstream(bd) and
-                self.__git.is_ancestor_or_equal(branch, bd) and
-                (self.__get_overridden_fork_point(bd) or self.__git.get_commit_sha_by_revision(branch) == self.fork_point(bd, use_overrides=False)))
+                not self.__is_merged_to_upstream(
+                    bd, opt_no_detect_squash_merges=False) and
+                self.__git.is_ancestor_or_equal(branch.full_name(), bd.full_name()) and
+                (self.__get_overridden_fork_point(bd) or self.__git.get_commit_sha_by_revision(branch) == self.fork_point(bd, use_overrides=False, opt_no_detect_squash_merges=False)))
 
         candidate_downstreams = list(filter(connected_with_green_edge, self.__down_branches[branch]))
         if not candidate_downstreams:
@@ -541,7 +582,7 @@ class MacheteClient:
                 f"No downstream (child) branch of `{branch}` is connected to "
                 f"`{branch}` with a green edge")
         if len(candidate_downstreams) > 1:
-            if self.__cli_opts.opt_yes:
+            if opt_yes:
                 raise MacheteException(
                     f"More than one downstream (child) branch of `{branch}` is "
                     f"connected to `{branch}` with a green edge and `-y/--yes` option is specified")
@@ -554,16 +595,16 @@ class MacheteClient:
             down_branch = candidate_downstreams[0]
             ans = self.ask_if(
                 f"Fast-forward {bold(branch)} to match {bold(down_branch)}?" + get_pretty_choices('y', 'N'),
-                f"Fast-forwarding {bold(branch)} to match {bold(down_branch)}...")
+                f"Fast-forwarding {bold(branch)} to match {bold(down_branch)}...", opt_yes=opt_yes)
             if ans in ('y', 'yes'):
                 self.__git.merge_fast_forward_only(down_branch)
             else:
                 return
 
         ans = self.ask_if(f"\nBranch {bold(down_branch)} is now merged into {bold(branch)}. Slide {bold(down_branch)} out of the tree of branch dependencies?" + get_pretty_choices('y', 'N'),
-                          f"\nBranch {bold(down_branch)} is now merged into {bold(branch)}. Sliding {bold(down_branch)} out of the tree of branch dependencies...")
+                          f"\nBranch {bold(down_branch)} is now merged into {bold(branch)}. Sliding {bold(down_branch)} out of the tree of branch dependencies...", opt_yes=opt_yes)
         if ans in ('y', 'yes'):
-            dds = self.__down_branches.get(down_branch, [])
+            dds = self.__down_branches.get(LocalBranchShortName.of(down_branch), [])
             for dd in dds:
                 self.up_branch[dd] = branch
             self.__down_branches[branch] = flat_map(
@@ -577,13 +618,27 @@ class MacheteClient:
             print("")
         self.__empty_line_status = new_status
 
-    def traverse(self) -> None:
-
+    def traverse(
+            self,
+            *,
+            opt_fetch: bool,
+            opt_list_commits: bool,
+            opt_merge: bool,
+            opt_no_detect_squash_merges: bool,
+            opt_no_edit_merge: bool,
+            opt_no_interactive_rebase: bool,
+            opt_push_tracked: bool,
+            opt_push_untracked: bool,
+            opt_return_to: str,
+            opt_start_from: str,
+            opt_yes: bool
+    ) -> None:
         self.expect_at_least_one_managed_branch()
 
         self.__empty_line_status = True
+        any_action_suggested: bool = False
 
-        if self.__cli_opts.opt_fetch:
+        if opt_fetch:
             for remote in self.__git.get_remotes():
                 print(f"Fetching {remote}...")
                 self.__git.fetch_remote(remote)
@@ -593,13 +648,13 @@ class MacheteClient:
 
         initial_branch = nearest_remaining_branch = self.__git.get_current_branch()
 
-        if self.__cli_opts.opt_start_from == "root":
+        if opt_start_from == "root":
             dest = self.root_branch(self.__git.get_current_branch(), if_unmanaged=PICK_FIRST_ROOT)
             self.__print_new_line(False)
             print(f"Checking out the root branch ({bold(dest)})")
             self.__git.checkout(dest)
             current_branch = dest
-        elif self.__cli_opts.opt_start_from == "first-root":
+        elif opt_start_from == "first-root":
             # Note that we already ensured that there is at least one managed branch.
             dest = self.managed_branches[0]
             self.__print_new_line(False)
@@ -610,17 +665,19 @@ class MacheteClient:
             current_branch = self.__git.get_current_branch()
             self.expect_in_managed_branches(current_branch)
 
-        branch: str
+        branch: LocalBranchShortName
         for branch in itertools.dropwhile(lambda x: x != current_branch, self.managed_branches):
             upstream = self.up_branch.get(branch)
 
-            needs_slide_out: bool = self.__is_merged_to_upstream(branch)
+            needs_slide_out: bool = self.__is_merged_to_upstream(
+                branch, opt_no_detect_squash_merges=opt_no_detect_squash_merges)
             s, remote = self.__git.get_strict_remote_sync_status(branch)
-            statuses_to_sync = (UNTRACKED,
-                                AHEAD_OF_REMOTE,
-                                BEHIND_REMOTE,
-                                DIVERGED_FROM_AND_OLDER_THAN_REMOTE,
-                                DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
+            statuses_to_sync = (
+                SyncToRemoteStatuses.UNTRACKED,
+                SyncToRemoteStatuses.AHEAD_OF_REMOTE,
+                SyncToRemoteStatuses.BEHIND_REMOTE,
+                SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE,
+                SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
             needs_remote_sync = s in statuses_to_sync
 
             if needs_slide_out:
@@ -628,20 +685,22 @@ class MacheteClient:
                 # branch qualifies for slide out;
                 # neither rebase nor merge will be suggested in such case anyway.
                 needs_parent_sync: bool = False
-            elif s == DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+            elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
                 # Avoid unnecessary fork point check if we already know that the
                 # branch qualifies for resetting to remote counterpart;
                 # neither rebase nor merge will be suggested in such case anyway.
                 needs_parent_sync = False
-            elif self.__cli_opts.opt_merge:
+            elif opt_merge:
                 needs_parent_sync = bool(
-                    upstream and not self.__git.is_ancestor_or_equal(upstream, branch))
+                    upstream and not self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()))
             else:  # using rebase
                 needs_parent_sync = bool(
                     upstream and
-                    not (self.__git.is_ancestor_or_equal(upstream, branch) and
+                    not (self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()) and
                          (self.__git.get_commit_sha_by_revision(upstream) ==
-                          self.fork_point(branch, use_overrides=True)))
+                          self.fork_point(
+                              branch, use_overrides=True,
+                              opt_no_detect_squash_merges=opt_no_detect_squash_merges)))
                 )
 
             if branch != current_branch and (needs_slide_out or needs_parent_sync or needs_remote_sync):
@@ -650,12 +709,17 @@ class MacheteClient:
                 self.__git.checkout(branch)
                 current_branch = branch
                 self.__print_new_line(False)
-                self.status(warn_on_yellow_edges=True)
+                self.status(
+                    warn_on_yellow_edges=True,
+                    opt_list_commits=opt_list_commits,
+                    opt_list_commits_with_hashes=False,
+                    opt_no_detect_squash_merges=opt_no_detect_squash_merges)
                 self.__print_new_line(True)
             if needs_slide_out:
+                any_action_suggested = True
                 self.__print_new_line(False)
                 ans: str = self.ask_if(f"Branch {bold(branch)} is merged into {bold(upstream)}. Slide {bold(branch)} out of the tree of branch dependencies?" + get_pretty_choices('y', 'N', 'q', 'yq'),
-                                       f"Branch {bold(branch)} is merged into {bold(upstream)}. Sliding {bold(branch)} out of the tree of branch dependencies...")
+                                       f"Branch {bold(branch)} is merged into {bold(upstream)}. Sliding {bold(branch)} out of the tree of branch dependencies...", opt_yes=opt_yes)
                 if ans in ('y', 'yes', 'yq'):
                     if nearest_remaining_branch == branch:
                         if self.__down_branches.get(branch):
@@ -681,16 +745,17 @@ class MacheteClient:
                 # If user answered 'no', we don't try to rebase/merge but still
                 # suggest to sync with remote (if needed; very rare in practice).
             elif needs_parent_sync:
+                any_action_suggested = True
                 self.__print_new_line(False)
-                if self.__cli_opts.opt_merge:
+                if opt_merge:
                     ans = self.ask_if(f"Merge {bold(upstream)} into {bold(branch)}?" + get_pretty_choices('y', 'N', 'q', 'yq'),
-                                      f"Merging {bold(upstream)} into {bold(branch)}...")
+                                      f"Merging {bold(upstream)} into {bold(branch)}...", opt_yes=opt_yes)
                 else:
                     ans = self.ask_if(f"Rebase {bold(branch)} onto {bold(upstream)}?" + get_pretty_choices('y', 'N', 'q', 'yq'),
-                                      f"Rebasing {bold(branch)} onto {bold(upstream)}...")
+                                      f"Rebasing {bold(branch)} onto {bold(upstream)}...", opt_yes=opt_yes)
                 if ans in ('y', 'yes', 'yq'):
-                    if self.__cli_opts.opt_merge:
-                        self.__git.merge(upstream, branch)
+                    if opt_merge:
+                        self.__git.merge(upstream, branch, opt_no_edit_merge)
                         # It's clearly possible that merge can be in progress
                         # after 'git merge' returned non-zero exit code;
                         # this happens most commonly in case of conflicts.
@@ -704,7 +769,12 @@ class MacheteClient:
                             sys.stdout.write("\nMerge in progress; stopping the traversal\n")
                             return
                     else:
-                        self.__git.rebase(f"refs/heads/{upstream}", self.fork_point(branch, use_overrides=True), branch)
+                        self.__git.rebase(
+                            LocalBranchShortName.of(upstream).full_name(), self.fork_point(
+                                branch,
+                                use_overrides=True,
+                                opt_no_detect_squash_merges=opt_no_detect_squash_merges),
+                            branch, opt_no_interactive_rebase)
                         # It's clearly possible that rebase can be in progress
                         # after 'git rebase' returned non-zero exit code;
                         # this happens most commonly in case of conflicts,
@@ -730,79 +800,113 @@ class MacheteClient:
                     return
 
             if needs_remote_sync:
+                any_action_suggested = True
                 try:
-                    if s == BEHIND_REMOTE:
-                        self.__handle_behind_state(current_branch, remote)
-                    elif s == AHEAD_OF_REMOTE:
+                    if s == SyncToRemoteStatuses.BEHIND_REMOTE:
+                        self.__handle_behind_state(current_branch, remote, opt_yes=opt_yes)
+                    elif s == SyncToRemoteStatuses.AHEAD_OF_REMOTE:
                         self.__handle_ahead_state(
-                            current_branch, remote, is_called_from_traverse=True)
-                    elif s == DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
-                        self.__handle_diverged_and_older_state(current_branch)
-                    elif s == DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
-                        self.__handle_diverged_and_newer_state(current_branch, remote)
-                    elif s == UNTRACKED:
-                        self.__handle_untracked_state(current_branch, is_called_from_traverse=True)
+                            current_branch=current_branch,
+                            remote=remote,
+                            is_called_from_traverse=True,
+                            opt_push_tracked=opt_push_tracked,
+                            opt_yes=opt_yes)
+                    elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+                        self.__handle_diverged_and_older_state(current_branch, opt_yes=opt_yes)
+                    elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
+                        self.__handle_diverged_and_newer_state(
+                            current_branch=current_branch,
+                            remote=remote,
+                            opt_push_tracked=opt_push_tracked,
+                            opt_yes=opt_yes)
+                    elif s == SyncToRemoteStatuses.UNTRACKED:
+                        self.__handle_untracked_state(
+                            branch=current_branch,
+                            is_called_from_traverse=True,
+                            opt_push_untracked=opt_push_untracked,
+                            opt_push_tracked=opt_push_tracked,
+                            opt_yes=opt_yes)
                 except StopInteraction:
                     return
 
-        if self.__cli_opts.opt_return_to == "here":
+        if opt_return_to == "here":
             self.__git.checkout(initial_branch)
-        elif self.__cli_opts.opt_return_to == "nearest-remaining":
+        elif opt_return_to == "nearest-remaining":
             self.__git.checkout(nearest_remaining_branch)
-        # otherwise cli_opts.opt_return_to == "stay", so no action is needed
+        # otherwise opt_return_to == "stay", so no action is needed
 
         self.__print_new_line(False)
-        self.status(warn_on_yellow_edges=True)
+        self.status(
+            warn_on_yellow_edges=True,
+            opt_list_commits=opt_list_commits,
+            opt_list_commits_with_hashes=False,
+            opt_no_detect_squash_merges=opt_no_detect_squash_merges)
         print("")
         if current_branch == self.managed_branches[-1]:
             msg: str = f"Reached branch {bold(current_branch)} which has no successor"
         else:
             msg = f"No successor of {bold(current_branch)} needs to be slid out or synced with upstream branch or remote"
         sys.stdout.write(f"{msg}; nothing left to update\n")
-
-        if self.__cli_opts.opt_return_to == "here" or (
-                self.__cli_opts.opt_return_to == "nearest-remaining" and nearest_remaining_branch == initial_branch):
+        if not any_action_suggested and initial_branch not in self.__roots:
+            sys.stdout.write(fmt("Tip: `traverse` by default starts from the current branch, "
+                                 "use flags (`--starts-from=`, `--whole` or `-w`, `-W`) to change this behavior.\n"
+                                 "Further info under `git machete traverse --help`.\n"))
+        if opt_return_to == "here" or (
+                opt_return_to == "nearest-remaining" and nearest_remaining_branch == initial_branch):
             print(f"Returned to the initial branch {bold(initial_branch)}")
-        elif self.__cli_opts.opt_return_to == "nearest-remaining" and nearest_remaining_branch != initial_branch:
+        elif opt_return_to == "nearest-remaining" and nearest_remaining_branch != initial_branch:
             print(
                 f"The initial branch {bold(initial_branch)} has been slid out. "
                 f"Returned to nearest remaining managed branch {bold(nearest_remaining_branch)}")
 
-    def status(self, warn_on_yellow_edges: bool) -> None:
+    def status(
+            self,
+            *,
+            warn_on_yellow_edges: bool,
+            opt_list_commits: bool,
+            opt_list_commits_with_hashes: bool,
+            opt_no_detect_squash_merges: bool
+    ) -> None:
         dfs_res = []
 
-        def prefix_dfs(u_: str, accumulated_path: List[Optional[str]]) -> None:
-            dfs_res.append((u_, accumulated_path))
+        def prefix_dfs(u_: LocalBranchShortName, accumulated_path_: List[Optional[LocalBranchShortName]]) -> None:
+            dfs_res.append((u_, accumulated_path_))
             if self.__down_branches.get(u_):
                 for (v, nv) in zip(self.__down_branches[u_][:-1], self.__down_branches[u_][1:]):
-                    prefix_dfs(v, accumulated_path + [nv])
-                prefix_dfs(self.__down_branches[u_][-1], accumulated_path + [None])
+                    prefix_dfs(v, accumulated_path_ + [nv])
+                prefix_dfs(self.__down_branches[u_][-1], accumulated_path_ + [None])
 
         for upstream in self.__roots:
-            prefix_dfs(upstream, accumulated_path=[])
+            prefix_dfs(upstream, accumulated_path_=[])
 
         out = io.StringIO()
-        edge_color: Dict[str, str] = {}
-        fp_sha_cached: Dict[str, Optional[str]] = {}  # TODO (#110): default dict with None
-        fp_branches_cached: Dict[str, List[BRANCH_DEF]] = {}
+        edge_color: Dict[LocalBranchShortName, str] = {}
+        fp_sha_cached: Dict[LocalBranchShortName, Optional[FullCommitHash]] = {}  # TODO (#110): default dict with None
+        fp_branches_cached: Dict[LocalBranchShortName, List[BranchDef]] = {}
 
-        def fp_sha(branch: str) -> Optional[str]:
+        def fp_sha(branch_: LocalBranchShortName) -> Optional[FullCommitHash]:
             if branch not in fp_sha_cached:
                 try:
                     # We're always using fork point overrides, even when status
                     # is launched from discover().
-                    fp_sha_cached[branch], fp_branches_cached[branch] = self.__fork_point_and_containing_branch_defs(branch, use_overrides=True)
+                    fp_sha_cached[branch_], fp_branches_cached[branch_] = self.__fork_point_and_containing_branch_defs(
+                        branch_,
+                        use_overrides=True,
+                        opt_no_detect_squash_merges=opt_no_detect_squash_merges)
                 except MacheteException:
-                    fp_sha_cached[branch], fp_branches_cached[branch] = None, []
-            return fp_sha_cached[branch]
+                    fp_sha_cached[branch_], fp_branches_cached[branch_] = None, []
+            return fp_sha_cached[branch_]
 
         # Edge colors need to be precomputed
         # in order to render the leading parts of lines properly.
         for branch in self.up_branch:
             upstream = self.up_branch[branch]
-            if self.is_merged_to(branch, upstream):
+            if self.is_merged_to(
+                    branch=branch,
+                    target=upstream,
+                    opt_no_detect_squash_merges=opt_no_detect_squash_merges):
                 edge_color[branch] = EscapeCodes.DIM
-            elif not self.__git.is_ancestor_or_equal(upstream, branch):
+            elif not self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()):
                 edge_color[branch] = EscapeCodes.RED
             elif self.__get_overridden_fork_point(branch) or self.__git.get_commit_sha_by_revision(upstream) == fp_sha(branch):
                 edge_color[branch] = EscapeCodes.GREEN
@@ -815,25 +919,25 @@ class MacheteClient:
         hook_path = self.__git.get_hook_path("machete-status-branch")
         hook_executable = self.__git.check_hook_executable(hook_path)
 
-        def print_line_prefix(b_: str, suffix: str) -> None:
+        def print_line_prefix(branch_: LocalBranchShortName, suffix: str) -> None:
             out.write("  ")
             for p in accumulated_path[:-1]:
                 if not p:
                     out.write("  ")
                 else:
                     out.write(colored(f"{utils.get_vertical_bar()} ", edge_color[p]))
-            out.write(colored(suffix, edge_color[b_]))
+            out.write(colored(suffix, edge_color[branch_]))
 
         for branch, accumulated_path in dfs_res:
             if branch in self.up_branch:
                 print_line_prefix(branch, f"{utils.get_vertical_bar()} \n")
-                if self.__cli_opts.opt_list_commits:
+                if opt_list_commits:
                     if edge_color[branch] in (EscapeCodes.RED, EscapeCodes.DIM):
-                        commits: List[Hash_ShortHash_Message] = self.__git.get_commits_between(fp_sha(branch), f"refs/heads/{branch}") if fp_sha(branch) else []
+                        commits: List[Hash_ShortHash_Message] = self.__git.get_commits_between(fp_sha(branch), branch.full_name()) if fp_sha(branch) else []
                     elif edge_color[branch] == EscapeCodes.YELLOW:
-                        commits = self.__git.get_commits_between(f"refs/heads/{self.up_branch[branch]}", f"refs/heads/{branch}")
+                        commits = self.__git.get_commits_between(self.up_branch[branch].full_name(), branch.full_name())
                     else:  # edge_color == EscapeCodes.GREEN
-                        commits = self.__git.get_commits_between(fp_sha(branch), f"refs/heads/{branch}")
+                        commits = self.__git.get_commits_between(fp_sha(branch), branch.full_name())
 
                     for sha, short_sha, subject in commits:
                         if sha == fp_sha(branch):
@@ -843,13 +947,13 @@ class MacheteClient:
                                 sorted(underline(lb_or_rb) for lb, lb_or_rb in fp_branches_cached[branch]))
                             fp_suffix: str = " %s %s %s seems to be a part of the unique history of %s" % \
                                              (colored(utils.get_right_arrow(), EscapeCodes.RED), colored("fork point ???", EscapeCodes.RED),
-                                              "this commit" if self.__cli_opts.opt_list_commits_with_hashes else f"commit {short_sha}",
+                                              "this commit" if opt_list_commits_with_hashes else f"commit {short_sha}",
                                               fp_branches_formatted)
                         else:
                             fp_suffix = ''
                         print_line_prefix(branch, utils.get_vertical_bar())
                         out.write(" %s%s%s\n" % (
-                                  f"{dim(short_sha)}  " if self.__cli_opts.opt_list_commits_with_hashes else "", dim(subject),
+                                  f"{dim(short_sha)}  " if opt_list_commits_with_hashes else "", dim(subject),
                                   fp_suffix))
                 elbow_ascii_only: Dict[str, str] = {EscapeCodes.DIM: "m-", EscapeCodes.RED: "x-", EscapeCodes.GREEN: "o-", EscapeCodes.YELLOW: "?-"}
                 elbow: str = u"└─" if not utils.ascii_only else elbow_ascii_only[edge_color[branch]]
@@ -880,14 +984,14 @@ class MacheteClient:
 
             s, remote = self.__git.get_combined_remote_sync_status(branch)
             sync_status = {
-                NO_REMOTES: "",
-                UNTRACKED: colored(" (untracked)", EscapeCodes.ORANGE),
-                IN_SYNC_WITH_REMOTE: "",
-                BEHIND_REMOTE: colored(f" (behind {remote})", EscapeCodes.RED),
-                AHEAD_OF_REMOTE: colored(f" (ahead of {remote})", EscapeCodes.RED),
-                DIVERGED_FROM_AND_OLDER_THAN_REMOTE: colored(f" (diverged from & older than {remote})", EscapeCodes.RED),
-                DIVERGED_FROM_AND_NEWER_THAN_REMOTE: colored(f" (diverged from {remote})", EscapeCodes.RED)
-            }[s]
+                SyncToRemoteStatuses.NO_REMOTES: "",
+                SyncToRemoteStatuses.UNTRACKED: colored(" (untracked)", EscapeCodes.ORANGE),
+                SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE: "",
+                SyncToRemoteStatuses.BEHIND_REMOTE: colored(f" (behind {remote})", EscapeCodes.RED),
+                SyncToRemoteStatuses.AHEAD_OF_REMOTE: colored(f" (ahead of {remote})", EscapeCodes.RED),
+                SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: colored(f" (diverged from & older than {remote})", EscapeCodes.RED),
+                SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: colored(f" (diverged from {remote})", EscapeCodes.RED)
+            }[SyncToRemoteStatuses(s)]
 
             hook_output = ""
             if hook_executable:
@@ -910,13 +1014,13 @@ class MacheteClient:
         if yellow_edge_branches and warn_on_yellow_edges:
             if len(yellow_edge_branches) == 1:
                 first_part = f"yellow edge indicates that fork point for `{yellow_edge_branches[0]}` is probably incorrectly inferred,\n" \
-                             f"or that some extra branch should be between `{self.up_branch[yellow_edge_branches[0]]}` and `{yellow_edge_branches[0]}`"
+                             f"or that some extra branch should be between `{self.up_branch[LocalBranchShortName.of(yellow_edge_branches[0])]}` and `{yellow_edge_branches[0]}`"
             else:
                 affected_branches = ", ".join(map(lambda x: f"`{x}`", yellow_edge_branches))
                 first_part = f"yellow edges indicate that fork points for {affected_branches} are probably incorrectly inferred" \
                              f"or that some extra branch should be added between each of these branches and its parent"
 
-            if not self.__cli_opts.opt_list_commits:
+            if not opt_list_commits:
                 second_part = "Run `git machete status --list-commits` or `git machete status --list-commits-with-hashes` to see more details"
             elif len(yellow_edge_branches) == 1:
                 second_part = f"Consider using `git machete fork-point --override-to=<revision>|--override-to-inferred|--override-to-parent {yellow_edge_branches[0]}`\n" \
@@ -928,7 +1032,7 @@ class MacheteClient:
             sys.stderr.write("\n")
             warn(f"{first_part}.\n\n{second_part}.")
 
-    def delete_unmanaged(self) -> None:
+    def delete_unmanaged(self, *, opt_yes: bool) -> None:
         branches_to_delete = excluding(self.__git.get_local_branches(), self.managed_branches)
         current_branch = self.__git.get_current_branch_or_none()
         if current_branch and current_branch in branches_to_delete:
@@ -940,11 +1044,11 @@ class MacheteClient:
             branches_to_delete_merged_to_head = [branch for branch in branches_to_delete if branch in branches_merged_to_head]
             for branch in branches_to_delete_merged_to_head:
                 remote_branch = self.__git.get_strict_counterpart_for_fetching_of_branch(branch)
-                is_merged_to_remote = self.__git.is_ancestor_or_equal(branch, remote_branch, later_prefix="refs/remotes/") if remote_branch else True
+                is_merged_to_remote = self.__git.is_ancestor_or_equal(branch.full_name(), remote_branch.full_name()) if remote_branch else True
                 msg_core = f"{bold(branch)} (merged to HEAD{'' if is_merged_to_remote else f', but not merged to {remote_branch}'})"
                 msg = f"Delete branch {msg_core}?" + get_pretty_choices('y', 'N', 'q')
                 opt_yes_msg = f"Deleting branch {msg_core}"
-                ans = self.ask_if(msg, opt_yes_msg)
+                ans = self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes)
                 if ans in ('y', 'yes'):
                     self.__git.delete_branch(branch, force=is_merged_to_remote)
                 elif ans in ('q', 'quit'):
@@ -955,7 +1059,7 @@ class MacheteClient:
                 msg_core = f"{bold(branch)} (unmerged to HEAD)"
                 msg = f"Delete branch {msg_core}?" + get_pretty_choices('y', 'N', 'q')
                 opt_yes_msg = f"Deleting branch {msg_core}"
-                ans = self.ask_if(msg, opt_yes_msg)
+                ans = self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes)
                 if ans in ('y', 'yes'):
                     self.__git.delete_branch(branch, force=True)
                 elif ans in ('q', 'quit'):
@@ -971,10 +1075,11 @@ class MacheteClient:
                 f"variable or edit {self._definition_file_path} directly.")
         return utils.run_cmd(default_editor_name, self._definition_file_path)
 
-    def __fork_point_and_containing_branch_defs(self, branch: str, use_overrides: bool) -> Tuple[Optional[str], List[BRANCH_DEF]]:
+    def __fork_point_and_containing_branch_defs(self, branch: LocalBranchShortName, use_overrides: bool, opt_no_detect_squash_merges: bool) -> Tuple[FullCommitHash, List[BranchDef]]:
         upstream = self.up_branch.get(branch)
 
-        if self.__is_merged_to_upstream(branch):
+        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_defs({branch})",
                   f"{branch} is merged to {upstream}; skipping inference, using tip of {branch} ({fp_sha}) as fork point")
@@ -983,7 +1088,7 @@ class MacheteClient:
         if use_overrides:
             overridden_fp_sha = self.__get_overridden_fork_point(branch)
             if overridden_fp_sha:
-                if upstream and self.__git.is_ancestor_or_equal(upstream, branch) and not self.__git.is_ancestor_or_equal(upstream, overridden_fp_sha, later_prefix=""):
+                if upstream and self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()) and not self.__git.is_ancestor_or_equal(upstream.full_name(), overridden_fp_sha.full_name()):
                     # We need to handle the case when branch is a descendant of upstream,
                     # but the fork point of branch is overridden to a commit that
                     # is NOT a descendant of upstream. In this case it's more
@@ -1000,7 +1105,7 @@ class MacheteClient:
         try:
             fp_sha, containing_branch_defs = next(self.__match_log_to_filtered_reflogs(branch))
         except StopIteration:
-            if upstream and self.__git.is_ancestor_or_equal(upstream, branch):
+            if upstream and self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()):
                 debug(f"fork_point_and_containing_branch_defs({branch})",
                       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), []
@@ -1012,7 +1117,7 @@ class MacheteClient:
                   "filtered reflog of any other branch or its remote counterpart "
                   f"(specifically: {' and '.join(map(utils.get_second, containing_branch_defs))})")
 
-            if upstream and self.__git.is_ancestor_or_equal(upstream, branch) and not self.__git.is_ancestor_or_equal(upstream, fp_sha, later_prefix=""):
+            if upstream and self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()) and not self.__git.is_ancestor_or_equal(upstream.full_name(), fp_sha.full_name()):
                 # That happens very rarely in practice (typically current head
                 # of any branch, including upstream, should occur on the reflog
                 # of this branch, thus is_ancestor(upstream, branch) should imply
@@ -1026,63 +1131,68 @@ class MacheteClient:
                       f"choosing commit {fp_sha} as fork point")
                 return fp_sha, containing_branch_defs
 
-    def fork_point(self, branch: str, use_overrides: bool) -> Optional[str]:
+    def fork_point(
+            self,
+            branch: LocalBranchShortName,
+            use_overrides: bool,
+            *,
+            opt_no_detect_squash_merges: bool
+    ) -> Optional[FullCommitHash]:
         sha, containing_branch_defs = self.__fork_point_and_containing_branch_defs(
-            branch, use_overrides)
-        return sha
+            branch, use_overrides, opt_no_detect_squash_merges=opt_no_detect_squash_merges)
+        return FullCommitHash.of(sha) if sha else None
 
-    def diff(self, branch: Optional[str]) -> None:
-        fp: str = self.fork_point(
+    def diff(self, *, branch: Optional[LocalBranchShortName], opt_stat: bool) -> None:
+        fp: FullCommitHash = self.fork_point(
             branch if branch else self.__git.get_current_branch(),
-            use_overrides=True)
+            use_overrides=True, opt_no_detect_squash_merges=False)
         self.__git.display_diff(
             branch=branch,
-            forkpoint=fp,
-            format_with_stat=self.__cli_opts.opt_stat)
+            fork_point=fp,
+            format_with_stat=opt_stat)
 
-    def log(self, branch: str) -> None:
-        full_branch_name = f"refs/heads{branch}"
-        forkpoint = self.fork_point(branch, use_overrides=True)
-        self.__git.display_branch_history_from_forkpoint(full_branch_name, forkpoint)
+    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)
 
-    def down(self, branch: str, pick_mode: bool) -> str:
+    def down(self, branch: LocalBranchShortName, pick_mode: bool) -> List[LocalBranchShortName]:
         self.expect_in_managed_branches(branch)
         dbs = self.__down_branches.get(branch)
         if not dbs:
             raise MacheteException(f"Branch `{branch}` has no downstream branch")
         elif len(dbs) == 1:
-            return dbs[0]
+            return [dbs[0]]
         elif pick_mode:
-            return self.pick(dbs, "downstream branch")
+            return [self.pick(dbs, "downstream branch")]
         else:
-            return "\n".join(dbs)
+            return dbs
 
-    def first_branch(self, branch: str) -> str:
+    def first_branch(self, branch: LocalBranchShortName) -> LocalBranchShortName:
         root = self.root_branch(branch, if_unmanaged=PICK_FIRST_ROOT)
         root_dbs = self.__down_branches.get(root)
         return root_dbs[0] if root_dbs else root
 
-    def last_branch(self, branch: str) -> str:
+    def last_branch(self, branch: LocalBranchShortName) -> LocalBranchShortName:
         destination = self.root_branch(branch, if_unmanaged=PICK_LAST_ROOT)
         while self.__down_branches.get(destination):
             destination = self.__down_branches[destination][-1]
         return destination
 
-    def next_branch(self, branch: str) -> str:
+    def next_branch(self, branch: LocalBranchShortName) -> LocalBranchShortName:
         self.expect_in_managed_branches(branch)
         index: int = self.managed_branches.index(branch) + 1
         if index == len(self.managed_branches):
             raise MacheteException(f"Branch `{branch}` has no successor")
         return self.managed_branches[index]
 
-    def prev_branch(self, branch: str) -> str:
+    def prev_branch(self, branch: LocalBranchShortName) -> LocalBranchShortName:
         self.expect_in_managed_branches(branch)
         index: int = self.managed_branches.index(branch) - 1
         if index == -1:
             raise MacheteException(f"Branch `{branch}` has no predecessor")
         return self.managed_branches[index]
 
-    def root_branch(self, branch: str, if_unmanaged: int) -> str:
+    def root_branch(self, branch: LocalBranchShortName, if_unmanaged: int) -> LocalBranchShortName:
         if branch not in self.managed_branches:
             if self.__roots:
                 if if_unmanaged == PICK_FIRST_ROOT:
@@ -1103,8 +1213,8 @@ class MacheteClient:
             upstream = self.up_branch.get(branch)
         return branch
 
-    def up(self, branch: str, prompt_if_inferred_msg: Optional[str],
-           prompt_if_inferred_yes_opt_msg: Optional[str]) -> str:
+    def up(self, branch: LocalBranchShortName, prompt_if_inferred_msg: Optional[str],
+           prompt_if_inferred_yes_opt_msg: Optional[str]) -> LocalBranchShortName:
         if branch in self.managed_branches:
             upstream = self.up_branch.get(branch)
             if upstream:
@@ -1117,11 +1227,11 @@ class MacheteClient:
                 if prompt_if_inferred_msg:
                     if self.ask_if(
                             prompt_if_inferred_msg % (branch, upstream),
-                            prompt_if_inferred_yes_opt_msg % (branch, upstream)
+                            prompt_if_inferred_yes_opt_msg % (branch, upstream),
+                            opt_yes=False
                     ) in ('y', 'yes'):
                         return upstream
-                    else:
-                        sys.exit(1)
+                    raise MacheteException("Aborting.")
                 else:
                     warn(
                         f"branch `{branch}` not found in the tree of branch "
@@ -1132,37 +1242,41 @@ class MacheteClient:
                     f"Branch `{branch}` not found in the tree of branch "
                     f"dependencies and its upstream could not be inferred")
 
-    def slidable(self) -> List[str]:
+    def get_slidable_branches(self) -> List[LocalBranchShortName]:
         return [branch for branch in self.managed_branches if branch in self.up_branch]
 
-    def slidable_after(self, branch: str) -> List[str]:
+    def get_slidable_after(self, branch: LocalBranchShortName) -> List[LocalBranchShortName]:
         if branch in self.up_branch:
             dbs = self.__down_branches.get(branch)
             if dbs and len(dbs) == 1:
                 return dbs
         return []
 
-    def __is_merged_to_upstream(self, branch: str) -> bool:
+    def __is_merged_to_upstream(
+            self, branch: LocalBranchShortName, *, opt_no_detect_squash_merges: bool) -> bool:
         if branch not in self.up_branch:
             return False
-        return self.is_merged_to(branch, self.up_branch[branch])
+        return self.is_merged_to(
+            branch,
+            self.up_branch[branch],
+            opt_no_detect_squash_merges=opt_no_detect_squash_merges)
 
-    def __run_post_slide_out_hook(self, new_upstream: str, slid_out_branch: str,
-                                  new_downstreams: List[str]) -> None:
+    def __run_post_slide_out_hook(self, new_upstream: LocalBranchShortName, slid_out_branch: LocalBranchShortName,
+                                  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})")
-            exit_code = utils.run_cmd(hook_path, new_upstream, slid_out_branch, *new_downstreams,
+            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())
             if exit_code != 0:
-                sys.stderr.write(
+                raise MacheteException(
                     f"The machete-post-slide-out hook exited with {exit_code}, aborting.\n")
-                sys.exit(exit_code)
 
-    def squash(self, current_branch: str, fork_commit: str) -> None:
+    def squash(self, *, current_branch: LocalBranchShortName, opt_fork_point: AnyRevision) -> None:
         commits: List[Hash_ShortHash_Message] = self.__git.get_commits_between(
-            fork_commit, current_branch)
+            opt_fork_point, current_branch)
         if not commits:
             raise MacheteException(
                 "No commits to squash. Use `-f` or `--fork-point` to specify the "
@@ -1175,11 +1289,11 @@ class MacheteClient:
             return
 
         earliest_sha, earliest_short_sha, earliest_subject = commits[0]
-        earliest_full_body = self.__git.get_commit_information("raw body", earliest_sha).strip()
+        earliest_full_body = self.__git.get_commit_information("raw body", FullCommitHash.of(earliest_sha)).strip()
         # %ai for ISO-8601 format; %aE/%aN for respecting .mailmap; see `git rev-list --help`
-        earliest_author_date = self.__git.get_commit_information("author date", earliest_sha).strip()
-        earliest_author_email = self.__git.get_commit_information("author email", earliest_sha).strip()
-        earliest_author_name = self.__git.get_commit_information("author name", earliest_sha).strip()
+        earliest_author_date = self.__git.get_commit_information("author date", FullCommitHash.of(earliest_sha)).strip()
+        earliest_author_email = self.__git.get_commit_information("author email", FullCommitHash.of(earliest_sha)).strip()
+        earliest_author_name = self.__git.get_commit_information("author name", FullCommitHash.of(earliest_sha)).strip()
 
         # Following the convention of `git cherry-pick`, `git commit --amend`, `git rebase` etc.,
         # let's retain the original author (only committer will be overwritten).
@@ -1192,14 +1306,14 @@ class MacheteClient:
         # The tree (HEAD^{tree}) argument must be passed as first,
         # otherwise the entire `commit-tree` will fail on some ancient supported
         # versions of git (at least on v1.7.10).
-        squashed_sha = self.__git.squash_commits_with_msg_and_new_env(
-            fork_commit, earliest_full_body, author_env).strip()
+        squashed_sha = FullCommitHash.of(self.__git.commit_tree_with_given_parent_and_message_and_env(
+            opt_fork_point, earliest_full_body, author_env).strip())
 
         # This can't be done with `git reset` since it doesn't allow for a custom reflog message.
         # Even worse, reset's reflog message would be filtered out in our fork point algorithm,
         # so the squashed commit would not even be considered to "belong"
         # (in the FP sense) to the current branch's history.
-        self.__git.update_head_ref_to_new_hash_with_msg(
+        self.__git.update_head_ref_to_new_hash_with_reflog_subject(
             squashed_sha, f"squash: {earliest_subject}")
 
         print(f"Squashed {len(commits)} commits:")
@@ -1213,7 +1327,7 @@ class MacheteClient:
         print()
         print(fmt(f"\t`git reset {latest_sha}`"))
 
-    def filtered_reflog(self, branch: str, prefix: str) -> List[str]:
+    def filtered_reflog(self, branch: AnyBranchName) -> List[FullCommitHash]:
         def is_excluded_reflog_subject(sha_: str, gs_: str) -> bool:
             is_excluded = (gs_.startswith("branch: Created from") or
                            gs_ == f"branch: Reset to {branch}" or
@@ -1222,30 +1336,30 @@ class MacheteClient:
                            gs_.startswith("fetch . ") or
                            # The rare case of a no-op rebase, the exact wording
                            # likely depends on git version
-                           gs_ == f"rebase finished: {prefix}{branch} onto {sha_}" or
-                           gs_ == f"rebase -i (finish): {prefix}{branch} onto {sha_}"
+                           gs_ == f"rebase finished: {branch.full_name()} onto {sha_}" or
+                           gs_ == f"rebase -i (finish): {branch.full_name()} onto {sha_}"
                            )
             if is_excluded:
                 debug(
-                    (f"filtered_reflog({branch}, {prefix}) -> is_excluded_reflog_subject({sha_},"
+                    (f"filtered_reflog({branch.full_name()}) -> is_excluded_reflog_subject({sha_},"
                      f" <<<{gs_}>>>)"),
                     "skipping reflog entry")
             return is_excluded
 
-        branch_reflog = self.__git.get_reflog(prefix + branch)
+        branch_reflog = self.__git.get_reflog(branch.full_name())
         if not branch_reflog:
             return []
 
         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}, {prefix})",
+            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}")
             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}, {prefix})",
+        debug(f"filtered_reflog({branch.full_name()})",
               "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
@@ -1267,8 +1381,8 @@ class MacheteClient:
                 fmt('Remotes are defined for this repository, but none of them '
                     'corresponds to GitHub (see `git remote -v` for details)'))
 
-        org: str
-        repo: str
+        org: str = ''
+        repo: str = ''
         if len(org_name_for_github_remote) == 1:
             org, repo = list(org_name_for_github_remote.values())[0]
         elif len(org_name_for_github_remote) > 1:
@@ -1287,63 +1401,63 @@ class MacheteClient:
 
     def __sync_annotations_to_definition_file(self, prs: List[GitHubPullRequest], current_user: Optional[str] = None) -> None:
         for pr in prs:
-            if pr.head in self.managed_branches:
+            if LocalBranchShortName.of(pr.head) in self.managed_branches:
                 debug('sync_annotations_to_definition_file()',
                       f'{pr} corresponds to a managed branch')
                 anno: str = f'PR #{pr.number}'
                 if pr.user != current_user:
                     anno += f' ({pr.user})'
-                upstream: Optional[str] = self.up_branch.get(pr.head)
+                upstream: Optional[LocalBranchShortName] = self.up_branch.get(LocalBranchShortName.of(pr.head))
                 if pr.base != upstream:
                     warn(f'branch `{pr.head}` has a different base in PR #{pr.number} (`{pr.base}`) '
                          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(pr.head) != anno:
-                    print(fmt(f'Annotating <b>{pr.head}</b> as `{anno}`'))
-                    self.__annotations[pr.head] = anno
+                if self.__annotations.get(LocalBranchShortName.of(pr.head)) != anno:
+                    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')
         self.save_definition_file()
 
     # Parse and evaluate direction against current branch for show/go commands
-    def parse_direction(self, param: str, branch: str, allow_current: bool, down_pick_mode: bool) -> str:
+    def parse_direction(self, param: str, branch: LocalBranchShortName, allow_current: bool, down_pick_mode: bool) -> List[LocalBranchShortName]:
         if param in ("c", "current") and allow_current:
-            return self.__git.get_current_branch()  # throws in case of detached HEAD, as in the spec
+            return [self.__git.get_current_branch()]  # throws in case of detached HEAD, as in the spec
         elif param in ("d", "down"):
             return self.down(branch, pick_mode=down_pick_mode)
         elif param in ("f", "first"):
-            return self.first_branch(branch)
+            return [self.first_branch(branch)]
         elif param in ("l", "last"):
-            return self.last_branch(branch)
+            return [self.last_branch(branch)]
         elif param in ("n", "next"):
-            return self.next_branch(branch)
+            return [self.next_branch(branch)]
         elif param in ("p", "prev"):
-            return self.prev_branch(branch)
+            return [self.prev_branch(branch)]
         elif param in ("r", "root"):
-            return self.root_branch(branch, if_unmanaged=PICK_FIRST_ROOT)
+            return [self.root_branch(branch, if_unmanaged=PICK_FIRST_ROOT)]
         elif param in ("u", "up"):
-            return self.up(branch, prompt_if_inferred_msg=None, prompt_if_inferred_yes_opt_msg=None)
+            return [self.up(branch, prompt_if_inferred_msg=None, prompt_if_inferred_yes_opt_msg=None)]
         else:
-            raise MacheteException(f"Invalid direction: `{param}` expected: {allowed_directions(allow_current)}")
+            raise MacheteException(f"Invalid direction: `{param}`; expected: {allowed_directions(allow_current)}")
 
-    def __match_log_to_filtered_reflogs(self, branch: str) -> Generator[Tuple[str, List[BRANCH_DEF]], None, None]:
+    def __match_log_to_filtered_reflogs(self, branch: LocalBranchShortName) -> Generator[Tuple[FullCommitHash, List[BranchDef]], None, None]:
 
         if branch not in self.__git.get_local_branches():
             raise MacheteException(f"`{branch}` is not a local branch")
 
         if self.__branch_defs_by_sha_in_reflog is None:
-            def generate_entries() -> Generator[Tuple[str, BRANCH_DEF], None, None]:
+            def generate_entries() -> Generator[Tuple[FullCommitHash, BranchDef], None, None]:
                 for lb in self.__git.get_local_branches():
                     lb_shas = set()
-                    for sha_ in self.filtered_reflog(lb, prefix="refs/heads/"):
+                    for sha_ in self.filtered_reflog(lb):
                         lb_shas.add(sha_)
-                        yield sha_, (lb, lb)
+                        yield FullCommitHash.of(sha_), (lb, str(lb))
                     remote_branch = self.__git.get_combined_counterpart_for_fetching_of_branch(lb)
                     if remote_branch:
-                        for sha_ in self.filtered_reflog(remote_branch, prefix="refs/remotes/"):
+                        for sha_ in self.filtered_reflog(remote_branch):
                             if sha_ not in lb_shas:
-                                yield sha_, (lb, remote_branch)
+                                yield FullCommitHash.of(sha_), (lb, str(remote_branch))
 
             self.__branch_defs_by_sha_in_reflog = {}
             for sha, branch_def in generate_entries():
@@ -1357,13 +1471,13 @@ class MacheteClient:
                     self.__branch_defs_by_sha_in_reflog[sha] = [branch_def]
 
             def log_result() -> Generator[str, None, None]:
-                branch_defs: List[BRANCH_DEF]
-                sha_: str
-                for sha_, branch_defs in self.__branch_defs_by_sha_in_reflog.items():
+                branch_defs_: List[BranchDef]
+                sha_: FullCommitHash
+                for sha_, branch_defs_ in self.__branch_defs_by_sha_in_reflog.items():
                     def branch_def_to_str(lb: str, lb_or_rb: str) -> str:
                         return lb if lb == lb_or_rb else f"{lb_or_rb} (remote counterpart of {lb})"
 
-                    joined_branch_defs = ", ".join(map(tupled(branch_def_to_str), branch_defs))
+                    joined_branch_defs = ", ".join(map(tupled(branch_def_to_str), branch_defs_))
                     yield dim(f"{sha_} => {joined_branch_defs}")
 
             debug(f"match_log_to_filtered_reflogs({branch})",
@@ -1374,7 +1488,7 @@ class MacheteClient:
                 # The entries must be sorted by lb_or_rb to make sure the
                 # upstream inference is deterministic (and does not depend on the
                 # order in which `generate_entries` iterated through the local branches).
-                branch_defs: List[BRANCH_DEF] = self.__branch_defs_by_sha_in_reflog[sha]
+                branch_defs: List[BranchDef] = self.__branch_defs_by_sha_in_reflog[sha]
 
                 def lb_is_not_b(lb: str, lb_or_rb: str) -> bool:
                     return lb != branch
@@ -1391,7 +1505,7 @@ class MacheteClient:
                 debug(f"match_log_to_filtered_reflogs({branch})",
                       f"commit {sha} not found in any filtered reflog")
 
-    def __infer_upstream(self, branch: str, condition: Callable[[str], bool] = lambda upstream: True, reject_reason_message: str = "") -> Optional[str]:
+    def __infer_upstream(self, branch: LocalBranchShortName, condition: Callable[[LocalBranchShortName], bool] = lambda upstream: True, reject_reason_message: str = "") -> Optional[LocalBranchShortName]:
         for sha, containing_branch_defs 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_defs))}")
@@ -1410,23 +1524,23 @@ class MacheteClient:
         return None
 
     @staticmethod
-    def config_key_for_override_fork_point_to(branch: str) -> str:
+    def config_key_for_override_fork_point_to(branch: LocalBranchShortName) -> str:
         return f"machete.overrideForkPoint.{branch}.to"
 
     @staticmethod
-    def config_key_for_override_fork_point_while_descendant_of(branch: str) -> str:
+    def config_key_for_override_fork_point_while_descendant_of(branch: LocalBranchShortName) -> str:
         return f"machete.overrideForkPoint.{branch}.whileDescendantOf"
 
     # Also includes config that is incomplete (only one entry out of two) or otherwise invalid.
-    def has_any_fork_point_override_config(self, branch: str) -> bool:
+    def has_any_fork_point_override_config(self, branch: LocalBranchShortName) -> bool:
         return (self.__git.get_config_attr_or_none(self.config_key_for_override_fork_point_to(branch)) or
                 self.__git.get_config_attr_or_none(self.config_key_for_override_fork_point_while_descendant_of(branch))) is not None
 
-    def __get_fork_point_override_data(self, branch: str) -> Optional[Tuple[str, str]]:
+    def __get_fork_point_override_data(self, branch: LocalBranchShortName) -> Optional[Tuple[FullCommitHash, FullCommitHash]]:
         to_key = self.config_key_for_override_fork_point_to(branch)
-        to = self.__git.get_config_attr_or_none(to_key)
+        to = FullCommitHash.of(self.__git.get_config_attr_or_none(to_key) or "")
         while_descendant_of_key = self.config_key_for_override_fork_point_while_descendant_of(branch)
-        while_descendant_of = self.__git.get_config_attr_or_none(while_descendant_of_key)
+        while_descendant_of = FullCommitHash.of(self.__git.get_config_attr_or_none(while_descendant_of_key) or "")
         if not to and not while_descendant_of:
             return None
         if to and not while_descendant_of:
@@ -1436,8 +1550,8 @@ class MacheteClient:
             warn(f"{while_descendant_of_key} config is set but {to_key} config is missing")
             return None
 
-        to_sha: Optional[str] = self.__git.get_commit_sha_by_revision(to, prefix="")
-        while_descendant_of_sha: Optional[str] = self.__git.get_commit_sha_by_revision(while_descendant_of, prefix="")
+        to_sha: Optional[FullCommitHash] = self.__git.get_commit_sha_by_revision(to)
+        while_descendant_of_sha: Optional[FullCommitHash] = self.__git.get_commit_sha_by_revision(while_descendant_of)
         if not to_sha or not while_descendant_of_sha:
             if not to_sha:
                 warn(f"{to_key} config value `{to}` does not point to a valid commit")
@@ -1447,7 +1561,7 @@ class MacheteClient:
         # This check needs to be performed every time the config is retrieved.
         # We can't rely on the values being validated in set_fork_point_override(),
         # since the config could have been modified outside of git-machete.
-        if not self.__git.is_ancestor_or_equal(to_sha, while_descendant_of_sha, earlier_prefix="", later_prefix=""):
+        if not self.__git.is_ancestor_or_equal(to_sha.full_name(), while_descendant_of_sha.full_name()):
             warn(
                 f"commit {self.__git.get_short_commit_sha_by_revision(to)} pointed by {to_key} config "
                 f"is not an ancestor of commit {self.__git.get_short_commit_sha_by_revision(while_descendant_of)} "
@@ -1455,7 +1569,7 @@ class MacheteClient:
             return None
         return to_sha, while_descendant_of_sha
 
-    def __get_overridden_fork_point(self, branch: str) -> Optional[str]:
+    def __get_overridden_fork_point(self, branch: LocalBranchShortName) -> Optional[FullCommitHash]:
         override_data = self.__get_fork_point_override_data(branch)
         if not override_data:
             return None
@@ -1466,7 +1580,7 @@ class MacheteClient:
         # While the latter checks the sanity of fork point override configuration,
         # the former checks if the override still applies to wherever the given
         # branch currently points.
-        if not self.__git.is_ancestor_or_equal(while_descendant_of, branch, earlier_prefix=""):
+        if not self.__git.is_ancestor_or_equal(while_descendant_of.full_name(), branch.full_name()):
             warn(fmt(
                 f"since branch <b>{branch}</b> is no longer a descendant of commit {self.__git.get_short_commit_sha_by_revision(while_descendant_of)}, ",
                 f"the fork point override to commit {self.__git.get_short_commit_sha_by_revision(to)} no longer applies.\n",
@@ -1477,17 +1591,17 @@ class MacheteClient:
               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: str) -> None:
+    def unset_fork_point_override(self, branch: LocalBranchShortName) -> None:
         self.__git.unset_config_attr(self.config_key_for_override_fork_point_to(branch))
         self.__git.unset_config_attr(self.config_key_for_override_fork_point_while_descendant_of(branch))
 
-    def set_fork_point_override(self, branch: str, to_revision: str) -> None:
+    def set_fork_point_override(self, branch: LocalBranchShortName, to_revision: AnyRevision) -> None:
         if branch not in self.__git.get_local_branches():
             raise MacheteException(f"`{branch}` is not a local branch")
-        to_sha = self.__git.get_commit_sha_by_revision(to_revision, prefix="")
+        to_sha = self.__git.get_commit_sha_by_revision(to_revision)
         if not to_sha:
             raise MacheteException(f"Cannot find revision {to_revision}")
-        if not self.__git.is_ancestor_or_equal(to_sha, branch, earlier_prefix=""):
+        if not self.__git.is_ancestor_or_equal(to_sha.full_name(), branch.full_name()):
             raise MacheteException(
                 f"Cannot override fork point: {self.__git.get_revision_repr(to_revision)} is not an ancestor of {branch}")
 
@@ -1495,13 +1609,30 @@ class MacheteClient:
         self.__git.set_config_attr(to_key, to_sha)
 
         while_descendant_of_key = self.config_key_for_override_fork_point_while_descendant_of(branch)
-        b_sha = self.__git.get_commit_sha_by_revision(branch, prefix="refs/heads/")
-        self.__git.set_config_attr(while_descendant_of_key, b_sha)
+        branch_sha = self.__git.get_commit_sha_by_revision(branch)
+        self.__git.set_config_attr(while_descendant_of_key, branch_sha)
 
         sys.stdout.write(
-            fmt(f"Fork point for <b>{branch}</b> is overridden to <b>{self.__git.get_revision_repr(to_revision)}</b>.\n", f"This applies as long as {branch} points to (or is descendant of) its current head (commit {self.__git.get_short_commit_sha_by_revision(b_sha)}).\n\n", f"This information is stored under git config keys:\n  * `{to_key}`\n  * `{while_descendant_of_key}`\n\n", f"To unset this override, use:\n  `git machete fork-point --unset-override {branch}`\n"))
-
-    def __pick_remote(self, branch: str, is_called_from_traverse: bool) -> None:
+            fmt(
+                f"Fork point for <b>{branch}</b> is overridden to <b>"
+                f"{self.__git.get_revision_repr(to_revision)}</b>.\n",
+                f"This applies as long as {branch} points to (or is descendant of)"
+                " its current head (commit "
+                f"{self.__git.get_short_commit_sha_by_revision(branch_sha)}).\n\n",
+                f"This information is stored under git config keys:\n  * `{to_key}"
+                f"`\n  * `{while_descendant_of_key}`\n\n",
+                "To unset this override, use:\n  `git machete fork-point "
+                f"--unset-override {branch}`\n"))
+
+    def __pick_remote(
+            self,
+            *,
+            branch: LocalBranchShortName,
+            is_called_from_traverse: bool,
+            opt_push_untracked: bool,
+            opt_push_tracked: bool,
+            opt_yes: bool
+    ) -> None:
         rems = self.__git.get_remotes()
         print("\n".join(f"[{index + 1}] {rem}" for index, rem in enumerate(rems)))
         msg = f"Select number 1..{len(rems)} to specify the destination remote " \
@@ -1517,22 +1648,40 @@ class MacheteClient:
             index = int(ans) - 1
             if index not in range(len(rems)):
                 raise MacheteException(f"Invalid index: {index + 1}")
-            self.handle_untracked_branch(rems[index], branch, is_called_from_traverse)
+            self.handle_untracked_branch(
+                new_remote=rems[index],
+                branch=branch,
+                is_called_from_traverse=is_called_from_traverse,
+                opt_push_untracked=opt_push_untracked,
+                opt_push_tracked=opt_push_tracked,
+                opt_yes=opt_yes)
         except ValueError:
             if not is_called_from_traverse:
                 raise MacheteException('Could not establish remote repository, pull request creation interrupted.')
 
-    def handle_untracked_branch(self, new_remote: str, branch: str, is_called_from_traverse: bool) -> None:
+    def handle_untracked_branch(
+            self,
+            *,
+            new_remote: str,
+            branch: LocalBranchShortName,
+            is_called_from_traverse: bool,
+            opt_push_untracked: bool,
+            opt_push_tracked: bool,
+            opt_yes: bool
+    ) -> None:
         rems: List[str] = self.__git.get_remotes()
         can_pick_other_remote = len(rems) > 1
         other_remote_choice = "o[ther-remote]" if can_pick_other_remote else ""
-        remote_branch = f"{new_remote}/{branch}"
-        if not self.__git.get_commit_sha_by_revision(remote_branch, prefix="refs/remotes/"):
+        remote_branch = RemoteBranchShortName.of(f"{new_remote}/{branch}")
+        if not self.__git.get_commit_sha_by_revision(remote_branch):
             choices = get_pretty_choices(*('y', 'N', 'q', 'yq', other_remote_choice) if is_called_from_traverse else ('y', 'Q', other_remote_choice))
             ask_message = f"Push untracked branch {bold(branch)} to {bold(new_remote)}?" + choices
             ask_opt_yes_message = f"Pushing untracked branch {bold(branch)} to {bold(new_remote)}..."
-            ans = self.ask_if(ask_message, ask_opt_yes_message,
-                              override_answer=None if self.__git._cli_opts.opt_push_untracked else "N")
+            ans = self.ask_if(
+                ask_message,
+                ask_opt_yes_message,
+                opt_yes=opt_yes,
+                override_answer=None if opt_push_untracked else "N")
             if is_called_from_traverse:
                 if ans in ('y', 'yes', 'yq'):
                     self.__git.push(new_remote, branch)
@@ -1540,7 +1689,12 @@ class MacheteClient:
                         raise StopInteraction
                     self.flush_caches()
                 elif can_pick_other_remote and ans in ('o', 'other'):
-                    self.__pick_remote(branch, is_called_from_traverse)
+                    self.__pick_remote(
+                        branch=branch,
+                        is_called_from_traverse=is_called_from_traverse,
+                        opt_push_untracked=opt_push_untracked,
+                        opt_push_tracked=opt_push_tracked,
+                        opt_yes=opt_yes)
                 elif ans in ('q', 'quit'):
                     raise StopInteraction
                 return
@@ -1549,7 +1703,12 @@ class MacheteClient:
                     self.__git.push(new_remote, branch)
                     self.flush_caches()
                 elif can_pick_other_remote and ans in ('o', 'other'):
-                    self.__pick_remote(branch, is_called_from_traverse)
+                    self.__pick_remote(
+                        branch=branch,
+                        is_called_from_traverse=is_called_from_traverse,
+                        opt_push_untracked=opt_push_untracked,
+                        opt_push_tracked=opt_push_tracked,
+                        opt_yes=opt_yes)
                 elif ans in ('q', 'quit'):
                     raise StopInteraction
                 else:
@@ -1559,80 +1718,85 @@ class MacheteClient:
         relation: int = self.__git.get_relation_to_remote_counterpart(branch, remote_branch)
 
         message: str = {
-            IN_SYNC_WITH_REMOTE:
+            SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE:
                 f"Branch {bold(branch)} is untracked, but its remote counterpart candidate {bold(remote_branch)} already exists and both branches point to the same commit.",
-            BEHIND_REMOTE:
+            SyncToRemoteStatuses.BEHIND_REMOTE:
                 f"Branch {bold(branch)} is untracked, but its remote counterpart candidate {bold(remote_branch)} already exists and is ahead of {bold(branch)}.",
-            AHEAD_OF_REMOTE:
+            SyncToRemoteStatuses.AHEAD_OF_REMOTE:
                 f"Branch {bold(branch)} is untracked, but its remote counterpart candidate {bold(remote_branch)} already exists and is behind {bold(branch)}.",
-            DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
                 f"Branch {bold(branch)} is untracked, it diverged from its remote counterpart candidate {bold(remote_branch)}, and has {bold('older')} commits than {bold(remote_branch)}.",
-            DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
+            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
                 f"Branch {bold(branch)} is untracked, it diverged from its remote counterpart candidate {bold(remote_branch)}, and has {bold('newer')} commits than {bold(remote_branch)}."
-        }[relation]
+        }[SyncToRemoteStatuses(relation)]
 
         ask_message, ask_opt_yes_message = {
-            IN_SYNC_WITH_REMOTE: (
+            SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE: (
                 f"Set the remote of {bold(branch)} to {bold(new_remote)} without pushing or pulling?" + get_pretty_choices('y', 'N', 'q', 'yq', other_remote_choice),
                 f"Setting the remote of {bold(branch)} to {bold(new_remote)}..."
             ),
-            BEHIND_REMOTE: (
+            SyncToRemoteStatuses.BEHIND_REMOTE: (
                 f"Pull {bold(branch)} (fast-forward only) from {bold(new_remote)}?" + get_pretty_choices('y', 'N', 'q', 'yq', other_remote_choice),
                 f"Pulling {bold(branch)} (fast-forward only) from {bold(new_remote)}..."
             ),
-            AHEAD_OF_REMOTE: (
+            SyncToRemoteStatuses.AHEAD_OF_REMOTE: (
                 f"Push branch {bold(branch)} to {bold(new_remote)}?" + get_pretty_choices('y', 'N', 'q', 'yq', other_remote_choice),
                 f"Pushing branch {bold(branch)} to {bold(new_remote)}..."
             ),
-            DIVERGED_FROM_AND_OLDER_THAN_REMOTE: (
+            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: (
                 f"Reset branch {bold(branch)} to the commit pointed by {bold(remote_branch)}?" + get_pretty_choices('y', 'N', 'q', 'yq', other_remote_choice),
                 f"Resetting branch {bold(branch)} to the commit pointed by {bold(remote_branch)}..."
             ),
-            DIVERGED_FROM_AND_NEWER_THAN_REMOTE: (
+            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: (
                 f"Push branch {bold(branch)} with force-with-lease to {bold(new_remote)}?" + get_pretty_choices('y', 'N', 'q', 'yq', other_remote_choice),
                 f"Pushing branch {bold(branch)} with force-with-lease to {bold(new_remote)}..."
             )
-        }[relation]
+        }[SyncToRemoteStatuses(relation)]
 
         override_answer: Optional[str] = {
-            IN_SYNC_WITH_REMOTE: None,
-            BEHIND_REMOTE: None,
-            AHEAD_OF_REMOTE: None if self.__cli_opts.opt_push_tracked else "N",
-            DIVERGED_FROM_AND_OLDER_THAN_REMOTE: None,
-            DIVERGED_FROM_AND_NEWER_THAN_REMOTE: None if self.__cli_opts.opt_push_tracked else "N",
-        }[relation]
+            SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE: None,
+            SyncToRemoteStatuses.BEHIND_REMOTE: None,
+            SyncToRemoteStatuses.AHEAD_OF_REMOTE: None if opt_push_tracked else "N",
+            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: None,
+            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: None if opt_push_tracked else "N",
+        }[SyncToRemoteStatuses(relation)]
 
         yes_action: Callable[[], None] = {
-            IN_SYNC_WITH_REMOTE: lambda: self.__git.set_upstream_to(remote_branch),
-            BEHIND_REMOTE: lambda: self.__git.pull_ff_only(new_remote, remote_branch),
-            AHEAD_OF_REMOTE: lambda: self.__git.push(new_remote, branch),
-            DIVERGED_FROM_AND_OLDER_THAN_REMOTE: lambda: self.__git.reset_keep(remote_branch),
-            DIVERGED_FROM_AND_NEWER_THAN_REMOTE: lambda: self.__git.push(
+            SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE: lambda: self.__git.set_upstream_to(remote_branch),
+            SyncToRemoteStatuses.BEHIND_REMOTE: lambda: self.__git.pull_ff_only(new_remote, remote_branch),
+            SyncToRemoteStatuses.AHEAD_OF_REMOTE: lambda: self.__git.push(new_remote, branch),
+            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: lambda: self.__git.reset_keep(remote_branch),
+            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: lambda: self.__git.push(
                 new_remote, branch, force_with_lease=True)
-        }[relation]
+        }[SyncToRemoteStatuses(relation)]
 
         print(message)
-        ans = self.ask_if(ask_message, ask_opt_yes_message, override_answer=override_answer)
+        ans = self.ask_if(ask_message, ask_opt_yes_message, override_answer=override_answer, opt_yes=opt_yes)
         if ans in ('y', 'yes', 'yq'):
             yes_action()
             if ans == 'yq':
                 raise StopInteraction
             self.flush_caches()
         elif can_pick_other_remote and ans in ('o', 'other'):
-            self.__pick_remote(branch, is_called_from_traverse)
+            self.__pick_remote(
+                branch=branch,
+                is_called_from_traverse=is_called_from_traverse,
+                opt_push_untracked=opt_push_untracked,
+                opt_push_tracked=opt_push_tracked,
+                opt_yes=opt_yes)
         elif ans in ('q', 'quit'):
             raise StopInteraction
 
-    def is_merged_to(self, branch: str, target: str) -> bool:
-        if self.__git.is_ancestor_or_equal(branch, target):
+    def is_merged_to(self, branch: LocalBranchShortName, target: AnyBranchName, *, opt_no_detect_squash_merges: bool) -> bool:
+        if self.__git.is_ancestor_or_equal(branch.full_name(), target.full_name()):
             # If branch is ancestor of or equal to the target, we need to distinguish between the
             # case of branch being "recently" created from the target and the case of
             # branch being fast-forward-merged to the target.
             # The applied heuristics is to check if the filtered reflog of the branch
             # (reflog stripped of trivial events like branch creation, reset etc.)
             # is non-empty.
-            return bool(self.filtered_reflog(branch, prefix="refs/heads/"))
-        elif self.__cli_opts.opt_no_detect_squash_merges:
+            return bool(self.filtered_reflog(branch))
+        elif opt_no_detect_squash_merges:
             return False
         else:
             # In the default mode.
@@ -1640,18 +1804,23 @@ class MacheteClient:
             # then branch may be squash or rebase merged into target.
             return self.__git.does_contain_equivalent_tree(branch, target)
 
-    def ask_if(self, msg: str, opt_yes_msg: Optional[str],
-               override_answer: Optional[str] = None,
-               apply_fmt: bool = True) -> str:
+    @staticmethod
+    def ask_if(
+            msg: str,
+            opt_yes_msg: Optional[str],
+            opt_yes: bool,
+            override_answer: Optional[str] = None,
+            apply_fmt: bool = True
+    ) -> str:
         if override_answer:
             return override_answer
-        if self.__cli_opts.opt_yes and opt_yes_msg:
+        if opt_yes and opt_yes_msg:
             print(fmt(opt_yes_msg) if apply_fmt else opt_yes_msg)
             return 'y'
         return input(fmt(msg) if apply_fmt else msg).lower()
 
     @staticmethod
-    def pick(choices: List[str], name: str, apply_fmt: bool = True) -> str:
+    def pick(choices: List[LocalBranchShortName], name: str, apply_fmt: bool = True) -> LocalBranchShortName:
         xs: str = "".join(f"[{index + 1}] {x}\n" for index, x in enumerate(choices))
         msg: str = xs + f"Specify {name} or hit <return> to skip: "
         try:
@@ -1669,6 +1838,15 @@ class MacheteClient:
         self.__branch_defs_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:
+        if not self.__git.is_ancestor_or_equal(
+                earlier_revision=forkpoint_sha.full_name(),
+                later_revision=branch.full_name()):
+            raise MacheteException(
+                f"Forkpoint {forkpoint_sha} is not ancestor of or the tip "
+                f"of the {branch} branch.")
+
     def checkout_github_prs(self, pr_no: int) -> None:
         org: str
         repo: str
@@ -1676,6 +1854,8 @@ class MacheteClient:
         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:
@@ -1690,14 +1870,15 @@ class MacheteClient:
                     remote_to_fetch: str = remote_from_pr
                 else:
                     remote_to_fetch = remote_already_added
-                print(f"Fetching {remote_to_fetch}...")
-                self.__git.fetch_remote(remote_to_fetch)
+                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, pr.head)
+            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.')
@@ -1705,31 +1886,35 @@ class MacheteClient:
 
         all_open_prs: List[GitHubPullRequest] = derive_pull_requests(org, repo)
 
-        self.__cli_opts.opt_yes = True  # TODO (#161): pass only needed options to methods
-        path: List[str] = self.__get_path_from_pr_chain(pr, all_open_prs)
-        reversed_path: List[str] = path[::-1]  # need to add from root downwards
+        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.__cli_opts.opt_as_root = True
-                    self.add(branch)
-                    self.__cli_opts.opt_as_root = False
+                    self.add(
+                        branch=branch,
+                        opt_as_root=True,
+                        opt_onto=None,
+                        opt_yes=True)
                 else:
-                    self.__cli_opts.opt_onto = reversed_path[index - 1]
-                    self.add(branch)
-        self.__cli_opts.opt_yes = False
+                    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(pr.head)
+        self.__git.checkout(LocalBranchShortName.of(pr.head))
         print(fmt(f"Pull request `#{pr.number}` checked out at local branch `{pr.head}`"))
 
-    def __get_path_from_pr_chain(self, current_pr: GitHubPullRequest, all_open_prs: List[GitHubPullRequest]) -> List[str]:
-        path: List[str] = [current_pr.head]
+    @staticmethod
+    def __get_path_from_pr_chain(current_pr: GitHubPullRequest, all_open_prs: List[GitHubPullRequest]) -> List[LocalBranchShortName]:
+        path: List[LocalBranchShortName] = [LocalBranchShortName.of(current_pr.head)]
         while current_pr:
-            path.append(current_pr.base)
+            path.append(LocalBranchShortName.of(current_pr.base))
             current_pr = utils.find_or_none(lambda x: x.head == current_pr.base, all_open_prs)
         return path
 
@@ -1749,7 +1934,7 @@ class MacheteClient:
                 return remote
         return None
 
-    def retarget_github_pr(self, head: str) -> None:
+    def retarget_github_pr(self, head: LocalBranchShortName) -> None:
         org: str
         repo: str
         _, (org, repo) = self.__derive_remote_and_github_org_and_repo()
@@ -1761,7 +1946,7 @@ class MacheteClient:
             raise MacheteException(f'No PR is opened in `{org}/{repo}` for branch `{head}`')
         debug(f'retarget_github_pr({head})', f'found {pr}')
 
-        new_base: Optional[str] = self.up_branch.get(head)
+        new_base: Optional[LocalBranchShortName] = self.up_branch.get(LocalBranchShortName.of(head))
         if not new_base:
             raise MacheteException(
                 f'Branch `{head}` does not have a parent branch (it is a root) '
@@ -1800,12 +1985,13 @@ class MacheteClient:
             f'Multiple non-origin remotes correspond to GitHub in this repository: '
             f'{", ".join(org_and_repo_for_github_remote.keys())}, aborting')
 
-    def create_github_pr(self, head: str, draft: bool) -> None:
+    def create_github_pr(
+            self, *, head: LocalBranchShortName, opt_draft: bool, opt_onto: Optional[LocalBranchShortName]) -> None:
         # first make sure that head branch is synced with remote
-        self.__sync_before_creating_pr()
+        self.__sync_before_creating_pr(opt_onto=opt_onto, opt_yes=False)
         self.flush_caches()
 
-        base: Optional[str] = self.up_branch.get(head)
+        base: Optional[LocalBranchShortName] = self.up_branch.get(LocalBranchShortName.of(head))
         org: str
         repo: str
         _, (org, repo) = self.__derive_remote_and_github_org_and_repo()
@@ -1818,9 +2004,9 @@ class MacheteClient:
         description: str = utils.slurp_file_or_empty(description_path)
 
         ok_str = ' <green><b>OK</b></green>'
-        print(fmt(f'Creating a {"draft " if draft else ""}PR from `{head}` to `{base}`...'), end='', flush=True)
+        print(fmt(f'Creating a {"draft " if opt_draft else ""}PR from `{head}` to `{base}`...'), end='', flush=True)
         pr: GitHubPullRequest = create_pull_request(org, repo, head=head, base=base, title=title,
-                                                    description=description, draft=draft)
+                                                    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')
@@ -1847,7 +2033,15 @@ class MacheteClient:
         self.__annotations[head] = f'PR #{pr.number}'
         self.save_definition_file()
 
-    def __handle_diverged_and_newer_state(self, current_branch: str, remote: str, is_called_from_traverse: bool = True) -> None:
+    def __handle_diverged_and_newer_state(
+            self,
+            *,
+            current_branch: LocalBranchShortName,
+            remote: str,
+            is_called_from_traverse: bool = True,
+            opt_push_tracked: bool,
+            opt_yes: bool
+    ) -> None:
         self.__print_new_line(False)
         remote_branch = self.__git.get_strict_counterpart_for_fetching_of_branch(current_branch)
         choices = get_pretty_choices(*('y', 'N', 'q', 'yq') if is_called_from_traverse else ('y', 'N', 'q'))
@@ -1856,7 +2050,7 @@ class MacheteClient:
             f"Push {bold(current_branch)} with force-with-lease to {bold(remote)}?" + choices,
             f"Branch {bold(current_branch)} diverged from (and has newer commits than) its remote counterpart {bold(remote_branch)}.\n"
             f"Pushing {bold(current_branch)} with force-with-lease to {bold(remote)}...",
-            override_answer=None if self.__cli_opts.opt_push_tracked else "N")
+            override_answer=None if opt_push_tracked else "N", opt_yes=opt_yes)
         if ans in ('y', 'yes', 'yq'):
             self.__git.push(remote, current_branch, force_with_lease=True)
             if ans == 'yq':
@@ -1864,49 +2058,86 @@ class MacheteClient:
                     raise StopInteraction
             self.flush_caches()
         elif ans in ('q', 'quit'):
-            if is_called_from_traverse:
-                raise StopInteraction
+            raise StopInteraction
 
-    def __handle_untracked_state(self, branch: str, is_called_from_traverse: bool) -> None:
+    def __handle_untracked_state(
+            self,
+            *,
+            branch: LocalBranchShortName,
+            is_called_from_traverse: bool,
+            opt_push_tracked: bool,
+            opt_push_untracked: bool,
+            opt_yes: bool
+    ) -> None:
         rems: List[str] = self.__git.get_remotes()
         rmt: Optional[str] = self.__git.get_inferred_remote_for_fetching_of_branch(branch)
         self.__print_new_line(False)
         if rmt:
-            self.handle_untracked_branch(rmt, branch, is_called_from_traverse)
+            self.handle_untracked_branch(
+                new_remote=rmt,
+                branch=branch,
+                is_called_from_traverse=is_called_from_traverse,
+                opt_push_untracked=opt_push_untracked,
+                opt_push_tracked=opt_push_tracked,
+                opt_yes=opt_yes)
         elif len(rems) == 1:
-            self.handle_untracked_branch(rems[0], branch, is_called_from_traverse)
+            self.handle_untracked_branch(
+                new_remote=rems[0], branch=branch,
+                is_called_from_traverse=is_called_from_traverse,
+                opt_push_untracked=opt_push_untracked,
+                opt_push_tracked=opt_push_tracked,
+                opt_yes=opt_yes)
         elif "origin" in rems:
-            self.handle_untracked_branch("origin", branch, is_called_from_traverse)
+            self.handle_untracked_branch(
+                new_remote="origin",
+                branch=branch,
+                is_called_from_traverse=is_called_from_traverse,
+                opt_push_untracked=opt_push_untracked,
+                opt_push_tracked=opt_push_tracked,
+                opt_yes=opt_yes)
         else:
             # We know that there is at least 1 remote, otherwise 's' would be 'NO_REMOTES'
             print(fmt(f"Branch `{bold(branch)}` is untracked and there's no `{bold('origin')}` repository."))
-            self.__pick_remote(branch, is_called_from_traverse)
-
-    def __handle_ahead_state(self, current_branch: str, remote: str, is_called_from_traverse: bool) -> None:
+            self.__pick_remote(
+                branch=branch,
+                is_called_from_traverse=is_called_from_traverse,
+                opt_push_untracked=opt_push_untracked,
+                opt_push_tracked=opt_push_tracked,
+                opt_yes=opt_yes)
+
+    def __handle_ahead_state(
+            self,
+            *,
+            current_branch: LocalBranchShortName,
+            remote: str,
+            is_called_from_traverse: bool,
+            opt_push_tracked: bool,
+            opt_yes: bool
+    ) -> None:
         self.__print_new_line(False)
         choices = get_pretty_choices(*('y', 'N', 'q', 'yq') if is_called_from_traverse else ('y', 'N', 'q'))
         ans = self.ask_if(
             f"Push {bold(current_branch)} to {bold(remote)}?" + choices,
             f"Pushing {bold(current_branch)} to {bold(remote)}...",
-            override_answer=None if self.__cli_opts.opt_push_tracked else "N"
+            override_answer=None if opt_push_tracked else "N",
+            opt_yes=opt_yes
         )
         if ans in ('y', 'yes', 'yq'):
             self.__git.push(remote, current_branch)
-            if ans == 'yq':
-                if is_called_from_traverse:
-                    raise StopInteraction
+            if ans == 'yq' and is_called_from_traverse:
+                raise StopInteraction
             self.flush_caches()
         elif ans in ('q', 'quit'):
-            if is_called_from_traverse:
-                raise StopInteraction
+            raise StopInteraction
 
-    def __handle_diverged_and_older_state(self, branch: str) -> None:
+    def __handle_diverged_and_older_state(self, branch: LocalBranchShortName, opt_yes: bool) -> None:
         self.__print_new_line(False)
         remote_branch = self.__git.get_strict_counterpart_for_fetching_of_branch(branch)
         ans = self.ask_if(
             f"Branch {bold(branch)} diverged from (and has older commits than) its remote counterpart {bold(remote_branch)}.\nReset branch {bold(branch)} to the commit pointed by {bold(remote_branch)}?" + get_pretty_choices(
                 'y', 'N', 'q', 'yq'),
-            f"Branch {bold(branch)} diverged from (and has older commits than) its remote counterpart {bold(remote_branch)}.\nResetting branch {bold(branch)} to the commit pointed by {bold(remote_branch)}...")
+            f"Branch {bold(branch)} diverged from (and has older commits than) its remote counterpart {bold(remote_branch)}.\nResetting branch {bold(branch)} to the commit pointed by {bold(remote_branch)}...",
+            opt_yes=opt_yes)
         if ans in ('y', 'yes', 'yq'):
             self.__git.reset_keep(remote_branch)
             if ans == 'yq':
@@ -1915,12 +2146,13 @@ class MacheteClient:
         elif ans in ('q', 'quit'):
             raise StopInteraction
 
-    def __handle_behind_state(self, branch: str, remote: str) -> None:
+    def __handle_behind_state(self, branch: LocalBranchShortName, remote: str, opt_yes: bool) -> None:
         remote_branch = self.__git.get_strict_counterpart_for_fetching_of_branch(branch)
         ans = self.ask_if(
             f"Branch {bold(branch)} is behind its remote counterpart {bold(remote_branch)}.\n"
             f"Pull {bold(branch)} (fast-forward only) from {bold(remote)}?" + get_pretty_choices('y', 'N', 'q', 'yq'),
-            f"Branch {bold(branch)} is behind its remote counterpart {bold(remote_branch)}.\nPulling {bold(branch)} (fast-forward only) from {bold(remote)}...")
+            f"Branch {bold(branch)} is behind its remote counterpart {bold(remote_branch)}.\nPulling {bold(branch)} (fast-forward only) from {bold(remote)}...",
+            opt_yes=opt_yes)
         if ans in ('y', 'yes', 'yq'):
             self.__git.pull_ff_only(remote, remote_branch)
             if ans == 'yq':
@@ -1930,14 +2162,14 @@ class MacheteClient:
         elif ans in ('q', 'quit'):
             raise StopInteraction
 
-    def __sync_before_creating_pr(self) -> None:
+    def __sync_before_creating_pr(self, *, opt_onto: Optional[LocalBranchShortName], opt_yes: bool) -> None:
 
         self.expect_at_least_one_managed_branch()
         self.__empty_line_status = True
 
         current_branch = self.__git.get_current_branch()
         if current_branch not in self.managed_branches:
-            self.add(current_branch)
+            self.add(branch=current_branch, opt_onto=opt_onto, opt_as_root=False, opt_yes=opt_yes)
             if current_branch not in self.managed_branches:
                 raise MacheteException(
                     "Command `github create-pr` can NOT be executed on the branch"
@@ -1947,45 +2179,66 @@ class MacheteClient:
                     "`discover` or `edit` or agree on adding the branch to the "
                     "definition file during the execution of `github create-pr` command.")
 
-        up_branch: Optional[str] = self.up_branch.get(current_branch)
+        up_branch: Optional[LocalBranchShortName] = self.up_branch.get(current_branch)
         if not up_branch:
             raise MacheteException(
                 f'Branch `{current_branch}` does not have a parent branch (it is a root), '
                 'base branch for the PR cannot be established.')
 
-        if self.__git.is_ancestor_or_equal(current_branch, up_branch):
+        if self.__git.is_ancestor_or_equal(current_branch.full_name(), up_branch.full_name()):
             raise MacheteException(
-                f'All commits in `{current_branch}` branch  are already included in `{up_branch}` branch.\nCannot create pull request.')
+                f'All commits in `{current_branch}` branch are already included in `{up_branch}` branch.\nCannot create pull request.')
 
         s, remote = self.__git.get_strict_remote_sync_status(current_branch)
-        statuses_to_sync = (UNTRACKED, AHEAD_OF_REMOTE, DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
+        statuses_to_sync = (
+            SyncToRemoteStatuses.UNTRACKED,
+            SyncToRemoteStatuses.AHEAD_OF_REMOTE,
+            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
         needs_remote_sync = s in statuses_to_sync
 
         if needs_remote_sync:
-            if s == AHEAD_OF_REMOTE:
-                self.__handle_ahead_state(current_branch, remote, is_called_from_traverse=False)
-            elif s == UNTRACKED:
-                self.__handle_untracked_state(current_branch, is_called_from_traverse=False)
-            elif s == DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
+            if s == SyncToRemoteStatuses.AHEAD_OF_REMOTE:
+                self.__handle_ahead_state(
+                    current_branch=current_branch,
+                    remote=remote,
+                    is_called_from_traverse=False,
+                    opt_push_tracked=True,
+                    opt_yes=opt_yes)
+            elif s == SyncToRemoteStatuses.UNTRACKED:
+                self.__handle_untracked_state(
+                    branch=current_branch,
+                    is_called_from_traverse=False,
+                    opt_push_tracked=True,
+                    opt_push_untracked=True,
+                    opt_yes=opt_yes)
+            elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
                 self.__handle_diverged_and_newer_state(
-                    current_branch, remote, is_called_from_traverse=False)
+                    current_branch=current_branch,
+                    remote=remote,
+                    is_called_from_traverse=False,
+                    opt_push_tracked=True,
+                    opt_yes=opt_yes)
 
             self.__print_new_line(False)
-            self.status(warn_on_yellow_edges=True)
+            self.status(
+                warn_on_yellow_edges=True,
+                opt_list_commits=False,
+                opt_list_commits_with_hashes=False,
+                opt_no_detect_squash_merges=False)
             self.__print_new_line(False)
 
         else:
-            if s == BEHIND_REMOTE:
+            if s == SyncToRemoteStatuses.BEHIND_REMOTE:
                 warn(f"Branch {current_branch} is in <b>BEHIND_REMOTE</b> state.\nConsider using 'git pull'.\n")
                 self.__print_new_line(False)
                 ans = self.ask_if("Proceed with pull request creation?" + get_pretty_choices('y', 'Q'),
-                                  "Proceeding with pull request creation...")
-            elif s == DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+                                  "Proceeding with pull request creation...", opt_yes=opt_yes)
+            elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
                 warn(f"Branch {current_branch} is in <b>DIVERGED_FROM_AND_OLDER_THAN_REMOTE</b> state.\nConsider using 'git reset --keep'.\n")
                 self.__print_new_line(False)
                 ans = self.ask_if("Proceed with pull request creation?" + get_pretty_choices('y', 'Q'),
-                                  "Proceeding with pull request creation...")
-            elif s == NO_REMOTES:
+                                  "Proceeding with pull request creation...", opt_yes=opt_yes)
+            elif s == SyncToRemoteStatuses.NO_REMOTES:
                 raise MacheteException(
                     "Could not create pull request - there are no remote repositories!")
             else:
@@ -1993,5 +2246,4 @@ class MacheteClient:
 
             if ans in ('y', 'yes'):
                 return
-            else:
-                raise MacheteException('Pull request creation interrupted.')
+            raise MacheteException('Pull request creation interrupted.')
diff --git a/git_machete/constants.py b/git_machete/constants.py
index 716276e..c89e541 100644
--- a/git_machete/constants.py
+++ b/git_machete/constants.py
@@ -1,3 +1,4 @@
+from enum import IntEnum
 import os
 
 MAX_COUNT_FOR_INITIAL_LOG = 10
@@ -21,14 +22,15 @@ class EscapeCodes:
     RED = '\033[91m'
 
 
-# TODO (#117): extract to namespace, use mypy
-NO_REMOTES = 0
-UNTRACKED = 1
-IN_SYNC_WITH_REMOTE = 2
-BEHIND_REMOTE = 3
-AHEAD_OF_REMOTE = 4
-DIVERGED_FROM_AND_OLDER_THAN_REMOTE = 5
-DIVERGED_FROM_AND_NEWER_THAN_REMOTE = 6
+class SyncToRemoteStatuses(IntEnum):
+    NO_REMOTES = 0
+    UNTRACKED = 1
+    IN_SYNC_WITH_REMOTE = 2
+    BEHIND_REMOTE = 3
+    AHEAD_OF_REMOTE = 4
+    DIVERGED_FROM_AND_OLDER_THAN_REMOTE = 5
+    DIVERGED_FROM_AND_NEWER_THAN_REMOTE = 6
+
 
 GIT_FORMAT_PATTERNS = {
     "author name": "%aN",
diff --git a/git_machete/docs.py b/git_machete/docs.py
index d5b9c3a..513561d 100644
--- a/git_machete/docs.py
+++ b/git_machete/docs.py
@@ -24,7 +24,7 @@ short_docs: Dict[str, str] = {
     "slide-out": "Slide out the current branch and sync its downstream (child) branches with its upstream (parent) branch via rebase or merge",
     "squash": "Squash the unique history of the current branch into a single commit",
     "status": "Display formatted tree of branch dependencies, including info on their sync with upstream branch and with remote",
-    "traverse": "Walk through the tree of branch dependencies and rebase, merge, slide out, push and/or pull each branch one by one",
+    "traverse": "Walk through the tree of branch dependencies and rebase, merge, slide out, push and/or pull each branch one by one. By default starts from current branch",
     "update": "Sync the current branch with its upstream (parent) branch via rebase or merge",
     "version": "Display the version and exit"
 }
@@ -305,8 +305,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.
 
         <b>`anno-prs`:</b>
 
@@ -382,7 +383,7 @@ long_docs: Dict[str, str] = {
             * exactly once during a `slide-out` execution (even if multiple branches are slid out),
             * zero or more times during `traverse` (every time a slide-out operation is confirmed).
 
-            If the hook returns a non-zero exit code, then the execution of the command is aborted,
+            If the hook returns a non-zero exit code, then an error is raised and the execution of the command is aborted,
             i.e. `slide-out` won't attempt rebase of the new downstream branches and `traverse` won't continue the traversal.
             In case of `advance` there is no difference (other than exit code of the entire `advance` command being non-zero),
             since slide-out is the last operation that happens within `advance`.
@@ -397,7 +398,7 @@ long_docs: Dict[str, str] = {
             1. what is going to be the new base for the rebased commits,
             2. what is the fork point - the place where the rebased history diverges from the upstream history,
             3. what branch is rebased.
-            If the hook returns a non-zero exit code, the entire rebase is aborted.
+            If the hook returns a non-zero exit code, an error is raised and the entire rebase is aborted.
 
             Note: this hook is independent from git's standard `pre-rebase` hook.
             If machete-pre-rebase returns zero, the execution flow continues to `git rebase`, which may also run `pre-rebase` hook if present.
@@ -610,7 +611,9 @@ long_docs: Dict[str, str] = {
                                        [--[no-]push] [--[no-]push-untracked]
                                        [--return-to=WHERE] [--start-from=WHERE] [-w|--whole] [-W] [-y|--yes]</b>
 
-        Traverses the branch dependency tree in pre-order (i.e. simply in the order as they occur in the definition file) and for each branch:
+        Traverses the branch dependency tree in pre-order (i.e. simply in the order as they occur in the definition file).
+        By default traverse starts from current branch, however this behaviour can be customized using options: `--starts-from=`, `--whole` or `-w`, `-W`.
+        For each branch, the command:
         * detects if the branch is merged to its parent/upstream
           - by commit equivalency (default), or by strict detection of merge commits (if `--no-detect-squash-merges` passed),
           - if so, asks the user whether to slide out the branch from the dependency tree (typically branches are longer needed after they're merged);
diff --git a/git_machete/exceptions.py b/git_machete/exceptions.py
index 8283ad0..a1f9893 100644
--- a/git_machete/exceptions.py
+++ b/git_machete/exceptions.py
@@ -14,14 +14,14 @@ class StopInteraction(Exception):
         pass
 
 
-class UnprocessableEntityHTTPError(Exception):
+class UnprocessableEntityHTTPError(MacheteException):
     """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.
     """
     def __init__(self, msg: str) -> None:
-        self.msg: str = msg + "\nPlease open an issue regarding this topic under link: https://github.com/VirtusLab/git-machete/issues/new"
+        self.msg: str = 'UnprocessableEntityHTTPError: ' + msg
 
     def __str__(self) -> str:
         return str(self.msg)
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 162bf4a..b969f91 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -4,41 +4,171 @@ import os
 import re
 import sys
 
-from git_machete.options import CommandLineOptions
 from git_machete.exceptions import MacheteException
 from git_machete.utils import colored, debug, fmt
 from git_machete import utils
-from git_machete.constants import AHEAD_OF_REMOTE, BEHIND_REMOTE, DIVERGED_FROM_AND_NEWER_THAN_REMOTE, \
-    DIVERGED_FROM_AND_OLDER_THAN_REMOTE, IN_SYNC_WITH_REMOTE, \
-    NO_REMOTES, MAX_COUNT_FOR_INITIAL_LOG, UNTRACKED, EscapeCodes, \
-    GIT_FORMAT_PATTERNS
+from git_machete.constants import (
+    MAX_COUNT_FOR_INITIAL_LOG, EscapeCodes, SyncToRemoteStatuses,
+    GIT_FORMAT_PATTERNS)
 
 
-REFLOG_ENTRY = Tuple[str, str]
+class AnyRevision(str):
+    @staticmethod
+    def of(value: str) -> Optional["AnyRevision"]:
+        return AnyRevision(value) if value else None
+
+    def full_name(self) -> "AnyRevision":
+        return self
+
+
+class AnyBranchName(AnyRevision):
+    @staticmethod
+    def of(value: str) -> Optional["AnyBranchName"]:
+        return AnyBranchName(value) if value else None
+
+    def full_name(self) -> "AnyBranchName":
+        return self
+
+
+class LocalBranchShortName(AnyBranchName):
+    @staticmethod
+    def of(value: str) -> Optional["LocalBranchShortName"]:
+        if value:
+            if 'refs/heads' in value or 'refs/remotes' in value:
+                raise TypeError(
+                    f'LocalBranchShortName cannot accept refs/heads or refs/remotes. Given value: {value}.\n'
+                    'Consider posting an issue on https://github.com/VirtusLab/git-machete/issues/new')
+            else:
+                return LocalBranchShortName(value)
+        return None
+
+    def full_name(self) -> Optional["LocalBranchFullName"]:
+        return LocalBranchFullName.from_short_name(self)
+
+
+class LocalBranchFullName(AnyBranchName):
+    @staticmethod
+    def of(value: str) -> Optional["LocalBranchFullName"]:
+        if value:
+            if 'refs/heads' in value:
+                return LocalBranchFullName(value)
+            else:
+                raise TypeError(f'LocalBranchFullName needs to have `refs/heads` prefix before branch name. Given value: {value}.\n'
+                                'Consider posting an issue on https://github.com/VirtusLab/git-machete/issues/new')
+        return None
+
+    @staticmethod
+    def from_short_name(value: LocalBranchShortName) -> Optional["LocalBranchFullName"]:
+        return LocalBranchFullName.of(f"refs/heads/{value}")
+
+    def full_name(self) -> "LocalBranchFullName":
+        return self
+
+    def to_short_name(self) -> "LocalBranchShortName":
+        return LocalBranchShortName.of(re.sub("^refs/heads/", "", self))
+
+
+class RemoteBranchShortName(AnyBranchName):
+    @staticmethod
+    def of(value: str) -> Optional["RemoteBranchShortName"]:
+        if value:
+            if 'refs/heads' in value or 'refs/remotes' in value:
+                raise TypeError(
+                    f'RemoteBranchShortName cannot accept refs/heads or refs/remotes. Given value: {value}.\n'
+                    'Consider posting an issue on https://github.com/VirtusLab/git-machete/issues/new')
+            else:
+                return RemoteBranchShortName(value)
+        return None
+
+    def full_name(self) -> Optional["RemoteBranchFullName"]:
+        return RemoteBranchFullName.from_short_name(self)
+
+
+class RemoteBranchFullName(AnyBranchName):
+    @staticmethod
+    def of(value: str) -> Optional["RemoteBranchFullName"]:
+        if value:
+            if 'refs/remotes' in value:
+                return RemoteBranchFullName(value)
+            else:
+                raise TypeError(f'RemoteBranchFullName needs to have `refs/remotes` prefix before branch name. Given value: {value}.\n'
+                                'Consider posting an issue on https://github.com/VirtusLab/git-machete/issues/new')
+        return None
+
+    @staticmethod
+    def from_short_name(value: RemoteBranchShortName) -> Optional["RemoteBranchFullName"]:
+        return RemoteBranchFullName.of(f"refs/remotes/{value}")
+
+    def full_name(self) -> "RemoteBranchFullName":
+        return self
+
+    def to_short_name(self) -> "RemoteBranchShortName":
+        return RemoteBranchShortName.of(re.sub("^refs/remotes/", "", self))
+
+
+class FullCommitHash(AnyRevision):
+    @staticmethod
+    def of(value: str) -> Optional["FullCommitHash"]:
+        value = value.strip()
+        if value:
+            if len(value) == 40:
+                return FullCommitHash(value)
+            else:
+                raise TypeError(f'FullCommitHash requires length of 40. Given value: {value}, has length: {len(value)}.\n'
+                                'Consider posting an issue on https://github.com/VirtusLab/git-machete/issues/new')
+        return None
+
+    def full_name(self) -> "FullCommitHash":
+        return self
+
+
+class ShortCommitHash(AnyRevision):
+    @staticmethod
+    def of(value: str) -> Optional["ShortCommitHash"]:
+        if value:
+            if len(value) == 7:
+                return ShortCommitHash(value)
+            else:
+                raise TypeError(f'ShortCommitHash requires length of 7. Given value: {value}, has length: {len(value)}.\n'
+                                'Consider posting an issue on https://github.com/VirtusLab/git-machete/issues/new')
+        return None
+
+    def full_name(self) -> "ShortCommitHash":
+        return self
+
+
+class FullTreeHash(str):
+    @staticmethod
+    def of(value: str) -> Optional["FullTreeHash"]:
+        return FullTreeHash(value) if value else None
+
+
+ReflogEntry = Tuple[FullCommitHash, str]
+
+HEAD = AnyRevision.of("HEAD")
 
 
 class GitContext:
 
-    def __init__(self, cli_opts: CommandLineOptions) -> None:
-        self._cli_opts: CommandLineOptions = cli_opts
+    def __init__(self) -> None:
         self._git_version: Optional[Tuple[int, ...]] = None
         self._root_dir: Optional[str] = None
         self._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
-        self.__counterparts_for_fetching_cached: Optional[Dict[str, Optional[str]]] = None  # TODO (#110): default dict with None
-        self.__short_commit_sha_by_revision_cached: Dict[str, str] = {}
-        self.__tree_sha_by_commit_sha_cached: Optional[Dict[str, Optional[str]]] = None  # TODO (#110): default dict with None
-        self.__commit_sha_by_revision_cached: Optional[Dict[str, Optional[str]]] = None  # TODO (#110): default dict with None
-        self.__committer_unix_timestamp_by_revision_cached: Optional[Dict[str, int]] = None  # TODO (#110): default dict with 0
-        self.__local_branches_cached: Optional[List[str]] = None
-        self.__remote_branches_cached: Optional[List[str]] = None
-        self.__initial_log_shas_cached: Dict[str, List[str]] = {}
-        self.__remaining_log_shas_cached: Dict[str, List[str]] = {}
-        self.__reflogs_cached: Optional[Dict[str, Optional[List[REFLOG_ENTRY]]]] = None
-        self.__merge_base_cached: Dict[Tuple[str, str], str] = {}
-        self.__contains_equivalent_tree_cached: Dict[Tuple[str, str], bool] = {}
+        self.__counterparts_for_fetching_cached: Optional[Dict[LocalBranchShortName, Optional[RemoteBranchShortName]]] = None  # TODO (#110): default dict with None
+        self.__short_commit_sha_by_revision_cached: Dict[AnyRevision, ShortCommitHash] = {}
+        self.__tree_sha_by_commit_sha_cached: Optional[Dict[FullCommitHash, Optional[FullTreeHash]]] = None  # TODO (#110): default dict with None
+        self.__commit_sha_by_revision_cached: Optional[Dict[AnyRevision, Optional[FullCommitHash]]] = None  # TODO (#110): default dict with None
+        self.__committer_unix_timestamp_by_revision_cached: Optional[Dict[AnyRevision, int]] = None  # TODO (#110): default dict with 0
+        self.__local_branches_cached: Optional[List[LocalBranchShortName]] = None
+        self.__remote_branches_cached: Optional[List[RemoteBranchShortName]] = None
+        self.__initial_log_shas_cached: Dict[LocalBranchShortName, List[FullCommitHash]] = {}
+        self.__remaining_log_shas_cached: Dict[LocalBranchShortName, List[FullCommitHash]] = {}
+        self.__reflogs_cached: Optional[Dict[AnyBranchName, Optional[List[ReflogEntry]]]] = None
+        self.__merge_base_cached: Dict[Tuple[FullCommitHash, FullCommitHash], FullCommitHash] = {}
+        self.__contains_equivalent_tree_cached: Dict[Tuple[FullCommitHash, FullCommitHash], bool] = {}
 
     @staticmethod
     def _run_git(git_cmd: str, *args: str, **kwargs: Dict[str, str]) -> int:
@@ -170,7 +300,7 @@ class GitContext:
 
     def fetch_remote(self, remote: str) -> None:
         if remote not in self.__fetch_done_for:
-            self._run_git("fetch", remote)
+            self._run_git("fetch", remote, "--prune")  # --prune added to remove remote branches that are already deleted from remote
             self.__fetch_done_for.add(remote)
             self.flush_caches()
 
@@ -178,18 +308,18 @@ class GitContext:
         self._run_git("fetch", remote, ref)
         self.flush_caches()
 
-    def set_upstream_to(self, remote_branch: str) -> None:
+    def set_upstream_to(self, remote_branch: RemoteBranchShortName) -> None:
         self._run_git("branch", "--set-upstream-to", remote_branch)
         self.flush_caches()
 
-    def reset_keep(self, to_revision: str) -> None:
+    def reset_keep(self, to_revision: AnyRevision) -> None:
         try:
             self._run_git("reset", "--keep", to_revision)
         except MacheteException:
             raise MacheteException(
                 f"Cannot perform `git reset --keep {to_revision}`. This is most likely caused by local uncommitted changes.")
 
-    def push(self, remote: str, branch: str, force_with_lease: bool = False) -> None:
+    def push(self, remote: str, branch: LocalBranchShortName, force_with_lease: bool = False) -> None:
         if not force_with_lease:
             opt_force = []
         elif self.get_git_version() >= (1, 8, 5):  # earliest version of git to support 'push --force-with-lease'
@@ -200,7 +330,7 @@ class GitContext:
         self._run_git("push", "--set-upstream", *(opt_force + args))
         self.flush_caches()
 
-    def pull_ff_only(self, remote: str, remote_branch: str) -> None:
+    def pull_ff_only(self, remote: str, remote_branch: RemoteBranchShortName) -> None:
         self.fetch_remote(remote)
         self._run_git("merge", "--ff-only", remote_branch)
         # There's apparently no way to set remote automatically when doing 'git pull' (as opposed to 'git push'),
@@ -208,37 +338,36 @@ class GitContext:
         self.set_upstream_to(remote_branch)
         self.flush_caches()
 
-    def __find_short_commit_sha_by_revision(self, revision: str) -> str:
-        return self._popen_git("rev-parse", "--short", revision + "^{commit}").rstrip()
+    def __find_short_commit_sha_by_revision(self, revision: AnyRevision) -> ShortCommitHash:
+        return ShortCommitHash.of(self._popen_git("rev-parse", "--short", revision + "^{commit}").rstrip())
 
-    def get_short_commit_sha_by_revision(self, revision: str) -> str:
+    def get_short_commit_sha_by_revision(self, revision: AnyRevision) -> ShortCommitHash:
         if revision not in self.__short_commit_sha_by_revision_cached:
             self.__short_commit_sha_by_revision_cached[revision] = self.__find_short_commit_sha_by_revision(revision)
         return self.__short_commit_sha_by_revision_cached[revision]
 
-    def __find_commit_sha_by_revision(self, revision: str) -> Optional[str]:
+    def __find_commit_sha_by_revision(self, revision: AnyRevision) -> Optional[FullCommitHash]:
         # Without ^{commit}, 'git rev-parse --verify' will not only accept references to other kinds of objects (like trees and blobs),
         # but just echo the argument (and exit successfully) even if the argument doesn't match anything in the object store.
         try:
-            return self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{commit}").rstrip()
+            return FullCommitHash.of(self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{commit}").rstrip())
         except MacheteException:
             return None
 
-    def get_commit_sha_by_revision(self, revision: str, prefix: str = "refs/heads/") -> Optional[str]:
+    def get_commit_sha_by_revision(self, revision: AnyRevision) -> Optional[FullCommitHash]:
         if self.__commit_sha_by_revision_cached is None:
             self.__load_branches()
-        full_revision: str = prefix + revision
-        if full_revision not in self.__commit_sha_by_revision_cached:
-            self.__commit_sha_by_revision_cached[full_revision] = self.__find_commit_sha_by_revision(full_revision)
-        return self.__commit_sha_by_revision_cached[full_revision]
+        if revision not in self.__commit_sha_by_revision_cached:
+            self.__commit_sha_by_revision_cached[revision] = self.__find_commit_sha_by_revision(revision)
+        return self.__commit_sha_by_revision_cached[revision]
 
-    def __find_tree_sha_by_revision(self, revision: str) -> Optional[str]:
+    def __find_tree_sha_by_revision(self, revision: AnyRevision) -> Optional[FullTreeHash]:
         try:
-            return self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{tree}").rstrip()
+            return FullTreeHash.of(self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{tree}").rstrip())
         except MacheteException:
             return None
 
-    def get_tree_sha_by_commit_sha(self, commit_sha: str) -> Optional[str]:
+    def get_tree_sha_by_commit_sha(self, commit_sha: FullCommitHash) -> Optional[FullTreeHash]:
         if self.__tree_sha_by_commit_sha_cached is None:
             self.__load_branches()
         if commit_sha not in self.__tree_sha_by_commit_sha_cached:
@@ -246,47 +375,47 @@ class GitContext:
         return self.__tree_sha_by_commit_sha_cached[commit_sha]
 
     @staticmethod
-    def is_full_sha(revision: str) -> Optional[Match[str]]:
+    def is_full_sha(revision: AnyRevision) -> Optional[Match[str]]:
         return re.match("^[0-9a-f]{40}$", revision)
 
     # Resolve a revision identifier to a full sha
-    def get_full_sha(self, revision: str, prefix: str = "refs/heads/") -> Optional[str]:
-        if prefix == "" and self.is_full_sha(revision):
-            return revision
+    def get_full_sha(self, revision: AnyRevision) -> Optional[FullCommitHash]:
+        if self.is_full_sha(revision.full_name()):
+            return FullCommitHash.of(revision)
         else:
-            return self.get_commit_sha_by_revision(revision, prefix)
+            return self.get_commit_sha_by_revision(revision)
 
-    def get_committer_unix_timestamp_by_revision(self, revision: str, prefix: str = "refs/heads/") -> int:
+    def get_committer_unix_timestamp_by_revision(self, revision: AnyBranchName) -> int:
         if self.__committer_unix_timestamp_by_revision_cached is None:
             self.__load_branches()
-        return self.__committer_unix_timestamp_by_revision_cached.get(prefix + revision, 0)
+        return self.__committer_unix_timestamp_by_revision_cached.get(revision.full_name(), 0)
 
-    def get_inferred_remote_for_fetching_of_branch(self, branch: str) -> Optional[str]:
+    def get_inferred_remote_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[str]:
         # Since many people don't use '--set-upstream' flag of 'push', we try to infer the remote instead.
         for remote in self.get_remotes():
             if f"{remote}/{branch}" in self.get_remote_branches():
                 return remote
         return None
 
-    def get_strict_remote_for_fetching_of_branch(self, branch: str) -> Optional[str]:
+    def get_strict_remote_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[str]:
         remote = self.get_config_attr_or_none(f"branch.{branch}.remote")
         return remote.rstrip() if remote else None
 
-    def get_combined_remote_for_fetching_of_branch(self, branch: str) -> Optional[str]:
+    def get_combined_remote_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[str]:
         return self.get_strict_remote_for_fetching_of_branch(branch) or self.get_inferred_remote_for_fetching_of_branch(branch)
 
-    def __get_inferred_counterpart_for_fetching_of_branch(self, branch: str) -> Optional[str]:
+    def __get_inferred_counterpart_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[RemoteBranchShortName]:
         for remote in self.get_remotes():
             if f"{remote}/{branch}" in self.get_remote_branches():
-                return f"{remote}/{branch}"
+                return RemoteBranchShortName.of(f"{remote}/{branch}")
         return None
 
-    def get_strict_counterpart_for_fetching_of_branch(self, branch: str) -> Optional[str]:
+    def get_strict_counterpart_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[RemoteBranchShortName]:
         if self.__counterparts_for_fetching_cached is None:
             self.__load_branches()
         return self.__counterparts_for_fetching_cached.get(branch)
 
-    def get_combined_counterpart_for_fetching_of_branch(self, branch: str) -> Optional[str]:
+    def get_combined_counterpart_for_fetching_of_branch(self, branch: LocalBranchShortName) -> Optional[RemoteBranchShortName]:
         # 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)
 
@@ -303,16 +432,16 @@ class GitContext:
     def is_revert_in_progress(self) -> bool:
         return os.path.isfile(self.get_git_subpath("REVERT_HEAD"))
 
-    def checkout(self, branch: str) -> None:
+    def checkout(self, branch: LocalBranchShortName) -> None:
         self._run_git("checkout", "--quiet", branch, "--")
         self.flush_caches()
 
-    def get_local_branches(self) -> List[str]:
+    def get_local_branches(self) -> List[LocalBranchShortName]:
         if self.__local_branches_cached is None:
             self.__load_branches()
         return self.__local_branches_cached
 
-    def get_remote_branches(self) -> List[str]:
+    def get_remote_branches(self) -> List[RemoteBranchShortName]:
         if self.__remote_branches_cached is None:
             self.__load_branches()
         return self.__remote_branches_cached
@@ -332,11 +461,11 @@ class GitContext:
             if len(values) != 4:
                 continue  # invalid, shouldn't happen
             branch, commit_sha, tree_sha, committer_unix_timestamp_and_time_zone = values
-            b_stripped = re.sub("^refs/remotes/", "", branch)
-            self.__remote_branches_cached += [b_stripped]
-            self.__commit_sha_by_revision_cached[branch] = commit_sha
-            self.__tree_sha_by_commit_sha_cached[commit_sha] = tree_sha
-            self.__committer_unix_timestamp_by_revision_cached[branch] = int(committer_unix_timestamp_and_time_zone.split(' ')[0])
+            b_stripped_remote = RemoteBranchFullName.of(branch).to_short_name()
+            self.__remote_branches_cached += [b_stripped_remote]
+            self.__commit_sha_by_revision_cached[RemoteBranchFullName.of(branch)] = FullCommitHash.of(commit_sha)
+            self.__tree_sha_by_commit_sha_cached[FullCommitHash.of(commit_sha)] = FullTreeHash.of(tree_sha)
+            self.__committer_unix_timestamp_by_revision_cached[RemoteBranchFullName.of(branch)] = int(committer_unix_timestamp_and_time_zone.split(' ')[0])
 
         raw_local = utils.get_non_empty_lines(self._popen_git("for-each-ref", "--format=%(refname)\t%(objectname)\t%(tree)\t%(committerdate:raw)\t%(upstream)", "refs/heads"))
 
@@ -345,39 +474,39 @@ class GitContext:
             if len(values) != 5:
                 continue  # invalid, shouldn't happen
             branch, commit_sha, tree_sha, committer_unix_timestamp_and_time_zone, fetch_counterpart = values
-            b_stripped = re.sub("^refs/heads/", "", branch)
-            fetch_counterpart_stripped = re.sub("^refs/remotes/", "", fetch_counterpart)
-            self.__local_branches_cached += [b_stripped]
-            self.__commit_sha_by_revision_cached[branch] = commit_sha
-            self.__tree_sha_by_commit_sha_cached[commit_sha] = tree_sha
-            self.__committer_unix_timestamp_by_revision_cached[branch] = int(committer_unix_timestamp_and_time_zone.split(' ')[0])
+            b_stripped_local = LocalBranchFullName.of(branch).to_short_name()
+            fetch_counterpart_stripped = RemoteBranchFullName.of(fetch_counterpart).to_short_name() if fetch_counterpart else None  # fetch_counterpart might be empty
+            self.__local_branches_cached += [b_stripped_local]
+            self.__commit_sha_by_revision_cached[LocalBranchFullName.of(branch)] = FullCommitHash.of(commit_sha)
+            self.__tree_sha_by_commit_sha_cached[FullCommitHash.of(commit_sha)] = FullTreeHash.of(tree_sha)
+            self.__committer_unix_timestamp_by_revision_cached[LocalBranchFullName.of(branch)] = int(committer_unix_timestamp_and_time_zone.split(' ')[0])
             if fetch_counterpart_stripped in self.__remote_branches_cached:
-                self.__counterparts_for_fetching_cached[b_stripped] = fetch_counterpart_stripped
+                self.__counterparts_for_fetching_cached[b_stripped_local] = fetch_counterpart_stripped
 
-    def __get_log_shas(self, revision: str, max_count: Optional[int]) -> List[str]:
-        opts = ([f"--max-count={str(max_count)}"] if max_count else []) + ["--format=%H", f"refs/heads/{revision}"]
-        return utils.get_non_empty_lines(self._popen_git("log", *opts))
+    def __get_log_shas(self, revision: AnyRevision, max_count: Optional[int]) -> List[FullCommitHash]:
+        opts = ([f"--max-count={str(max_count)}"] if max_count else []) + ["--format=%H", revision.full_name()]
+        return list(map(FullCommitHash.of, utils.get_non_empty_lines(self._popen_git("log", *opts))))
 
     # Since getting the full history of a branch can be an expensive operation for large repositories (compared to all other underlying git operations),
     # there's a simple optimization in place: we first fetch only a couple of first commits in the history,
     # and only fetch the rest if needed.
-    def spoonfeed_log_shas(self, branch: str) -> Generator[str, None, None]:
+    def spoonfeed_log_shas(self, branch: LocalBranchShortName) -> Generator[FullCommitHash, None, None]:
         if branch not in self.__initial_log_shas_cached:
             self.__initial_log_shas_cached[branch] = self.__get_log_shas(branch, max_count=MAX_COUNT_FOR_INITIAL_LOG)
         for sha in self.__initial_log_shas_cached[branch]:
-            yield sha
+            yield FullCommitHash.of(sha)
 
         if branch not in self.__remaining_log_shas_cached:
             self.__remaining_log_shas_cached[branch] = self.__get_log_shas(branch, max_count=None)[MAX_COUNT_FOR_INITIAL_LOG:]
         for sha in self.__remaining_log_shas_cached[branch]:
-            yield sha
+            yield FullCommitHash.of(sha)
 
     def __load_all_reflogs(self) -> None:
         # %gd - reflog selector (refname@{num})
         # %H - full hash
         # %gs - reflog subject
-        all_branches = [f"refs/heads/{branch}" for branch in self.get_local_branches()] + \
-                       [f"refs/remotes/{self.get_combined_counterpart_for_fetching_of_branch(branch)}" for branch in self.get_local_branches() if self.get_combined_counterpart_for_fetching_of_branch(branch)]
+        all_branches = [str(branch.full_name()) for branch in self.get_local_branches()] + \
+                       [str(self.get_combined_counterpart_for_fetching_of_branch(branch).full_name()) for branch in self.get_local_branches() if self.get_combined_counterpart_for_fetching_of_branch(branch)]  # str here to match _popen_git() input type
         # The trailing '--' is necessary to avoid ambiguity in case there is a file called just exactly like one of the branches.
         entries = utils.get_non_empty_lines(self._popen_git("reflog", "show", "--format=%gD\t%H\t%gs", *(all_branches + ["--"])))
         self.__reflogs_cached = {}
@@ -391,10 +520,10 @@ class GitContext:
                 continue
             branch, pos = branch_and_pos
             if branch not in self.__reflogs_cached:
-                self.__reflogs_cached[branch] = []
-            self.__reflogs_cached[branch] += [(sha, subject)]
+                self.__reflogs_cached[AnyBranchName.of(branch)] = []
+            self.__reflogs_cached[AnyBranchName.of(branch)] += [(FullCommitHash.of(sha), subject)]
 
-    def get_reflog(self, branch: str) -> List[REFLOG_ENTRY]:
+    def get_reflog(self, branch: AnyBranchName) -> List[ReflogEntry]:
         # git version 2.14.2 fixed a bug that caused fetching reflog of more than
         # one branch at the same time unreliable in certain cases
         if self.get_git_version() >= (2, 14, 2):
@@ -414,7 +543,7 @@ class GitContext:
                 ]
             return self.__reflogs_cached[branch]
 
-    def create_branch(self, branch: str, out_of_revision: str) -> None:
+    def create_branch(self, branch: LocalBranchShortName, out_of_revision: AnyRevision) -> None:
         self._run_git("checkout", "-b", branch, out_of_revision)
         self.flush_caches()  # the repository state has changed because of a successful branch creation, let's defensively flush all the caches
 
@@ -434,9 +563,9 @@ class GitContext:
         self.__merge_base_cached = {}
         self.__contains_equivalent_tree_cached = {}
 
-    def get_revision_repr(self, revision: str) -> str:
+    def get_revision_repr(self, revision: AnyRevision) -> str:
         short_sha = self.get_short_commit_sha_by_revision(revision)
-        if self.is_full_sha(revision) or revision == short_sha:
+        if self.is_full_sha(revision.full_name()) or revision == short_sha:
             return f"commit {revision}"
         else:
             return f"{revision} (commit {self.get_short_commit_sha_by_revision(revision)})"
@@ -444,7 +573,7 @@ class GitContext:
     # Note: while rebase is ongoing, the repository is always in a detached HEAD state,
     # so we need to extract the name of the currently rebased branch from the rebase-specific internals
     # rather than rely on 'git symbolic-ref HEAD' (i.e. the contents of .git/HEAD).
-    def get_currently_rebased_branch_or_none(self) -> Optional[str]:  # utils/private
+    def get_currently_rebased_branch_or_none(self) -> Optional[LocalBranchShortName]:  # utils/private
         # https://stackoverflow.com/questions/3921409
 
         head_name_file = None
@@ -465,12 +594,12 @@ class GitContext:
             return None
         with open(head_name_file) as f:
             raw = f.read().strip()
-            return re.sub("^refs/heads/", "", raw)
+            return LocalBranchFullName.of(raw).to_short_name()
 
-    def get_currently_checked_out_branch_or_none(self) -> Optional[str]:
+    def get_currently_checked_out_branch_or_none(self) -> Optional[LocalBranchShortName]:
         try:
             raw = self._popen_git("symbolic-ref", "--quiet", "HEAD").strip()
-            return re.sub("^refs/heads/", "", raw)
+            return LocalBranchFullName.of(raw).to_short_name()
         except MacheteException:
             return None
 
@@ -492,16 +621,16 @@ class GitContext:
             raise MacheteException(
                 "Revert in progress. Conclude the revert first with `git revert --continue` or `git revert --abort`.")
 
-    def get_current_branch_or_none(self) -> Optional[str]:
+    def get_current_branch_or_none(self) -> Optional[LocalBranchShortName]:
         return self.get_currently_checked_out_branch_or_none() or self.get_currently_rebased_branch_or_none()
 
-    def get_current_branch(self) -> str:
+    def get_current_branch(self) -> LocalBranchShortName:
         result = self.get_current_branch_or_none()
         if not result:
             raise MacheteException("Not currently on any branch")
         return result
 
-    def __get_merge_base(self, sha1: str, sha2: str) -> str:
+    def __get_merge_base(self, sha1: FullCommitHash, sha2: FullCommitHash) -> FullCommitHash:
         if sha1 > sha2:
             sha1, sha2 = sha2, sha1
         if not (sha1, sha2) in self.__merge_base_cached:
@@ -512,23 +641,24 @@ class GitContext:
             #   then there is exactly one merge-base - the ancestor,
             # * if neither of sha1, sha2 is an ancestor of another,
             #   then none of the (possibly more than one) merge-bases is equal to either of sha1/sha2 anyway.
-            self.__merge_base_cached[sha1, sha2] = self._popen_git("merge-base", sha1, sha2).rstrip()
+            self.__merge_base_cached[sha1, sha2] = FullCommitHash.of(self._popen_git("merge-base", sha1, sha2).rstrip())
         return self.__merge_base_cached[sha1, sha2]
 
     # Note: the 'git rev-parse --verify' validation is not performed in case for either of earlier/later
     # if the corresponding prefix is empty AND the revision is a 40 hex digit hash.
     def is_ancestor_or_equal(
             self,
-            earlier_revision: str,
-            later_revision: str,
-            earlier_prefix: str = "refs/heads/",
-            later_prefix: str = "refs/heads/",
+            earlier_revision: AnyRevision,
+            later_revision: AnyRevision,
     ) -> bool:
-        earlier_sha = self.get_full_sha(earlier_revision, earlier_prefix)
-        later_sha = self.get_full_sha(later_revision, later_prefix)
-
+        earlier_sha = self.get_full_sha(earlier_revision)
+        later_sha = self.get_full_sha(later_revision)
+        # This if statement is not changing the outcome of the later return, but
+        # it enhances the efficiency of the script. If both hashes are the same,
+        # there is no point running git merge-base.
         if earlier_sha == later_sha:
             return True
+
         return self.__get_merge_base(earlier_sha, later_sha) == earlier_sha
 
     # Determine if later_revision, or any ancestors of later_revision that are NOT ancestors of earlier_revision,
@@ -536,13 +666,11 @@ class GitContext:
     # later_revision contains a rebase or squash merge of earlier_revision.
     def does_contain_equivalent_tree(
             self,
-            earlier_revision: str,
-            later_revision: str,
-            earlier_prefix: str = "refs/heads/",
-            later_prefix: str = "refs/heads/",
+            earlier_revision: AnyRevision,
+            later_revision: AnyRevision,
     ) -> bool:
-        earlier_commit_sha = self.get_full_sha(earlier_revision, earlier_prefix)
-        later_commit_sha = self.get_full_sha(later_revision, later_prefix)
+        earlier_commit_sha = self.get_full_sha(earlier_revision)
+        later_commit_sha = self.get_full_sha(later_revision)
 
         if earlier_commit_sha == later_commit_sha:
             return True
@@ -572,8 +700,8 @@ class GitContext:
         self.__contains_equivalent_tree_cached[earlier_commit_sha, later_commit_sha] = result
         return result
 
-    def get_sole_remote_branch(self, branch: str) -> Optional[str]:
-        def matches(remote_branch: str) -> bool:
+    def get_sole_remote_branch(self, branch: LocalBranchShortName) -> Optional[RemoteBranchShortName]:
+        def matches(remote_branch: RemoteBranchShortName) -> bool:
             # Note that this matcher is defensively too inclusive:
             # if there is both origin/foo and origin/feature/foo,
             # then both are matched for 'foo';
@@ -584,9 +712,9 @@ class GitContext:
         matching_remote_branches = list(filter(matches, self.get_remote_branches()))
         return matching_remote_branches[0] if len(matching_remote_branches) == 1 else None
 
-    def get_merged_local_branches(self) -> List[str]:
+    def get_merged_local_branches(self) -> List[LocalBranchShortName]:
         return list(map(
-            lambda branch: re.sub("^refs/heads/", "", branch),
+            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"))
         ))
@@ -610,25 +738,25 @@ class GitContext:
         else:
             return True
 
-    def merge(self, branch: str, into: str) -> None:  # refs/heads/ prefix is assumed for 'branch'
-        extra_params = ["--no-edit"] if self._cli_opts.opt_no_edit_merge else ["--edit"]
+    def merge(self, branch: LocalBranchShortName, into: LocalBranchShortName, opt_no_edit_merge: bool) -> None:  # refs/heads/ prefix is assumed for 'branch'
+        extra_params = ["--no-edit"] if opt_no_edit_merge else ["--edit"]
         # We need to specify the message explicitly to avoid 'refs/heads/' prefix getting into the message...
         commit_message = f"Merge branch '{branch}' into {into}"
         # ...since we prepend 'refs/heads/' to the merged branch name for unambiguity.
-        self._run_git("merge", "-m", commit_message, f"refs/heads/{branch}", *extra_params)
+        self._run_git("merge", "-m", commit_message, branch.full_name(), *extra_params)
         self.flush_caches()
 
-    def merge_fast_forward_only(self, branch: str) -> None:  # refs/heads/ prefix is assumed for 'branch'
-        self._run_git("merge", "--ff-only", f"refs/heads/{branch}")
+    def merge_fast_forward_only(self, branch: LocalBranchShortName) -> None:  # refs/heads/ prefix is assumed for 'branch'
+        self._run_git("merge", "--ff-only", branch.full_name())
         self.flush_caches()
 
-    def rebase(self, onto: str, fork_commit: str, branch: str) -> None:
+    def rebase(self, onto: AnyRevision, fork_revision: AnyRevision, branch: LocalBranchShortName, opt_no_interactive_rebase: bool) -> None:
         def do_rebase() -> None:
             try:
-                if self._cli_opts.opt_no_interactive_rebase:
-                    self._run_git("rebase", "--onto", onto, fork_commit, branch)
+                if opt_no_interactive_rebase:
+                    self._run_git("rebase", "--onto", onto, fork_revision, branch)
                 else:
-                    self._run_git("rebase", "--interactive", "--onto", onto, fork_commit, branch)
+                    self._run_git("rebase", "--interactive", "--onto", onto, fork_revision, branch)
             finally:
                 # https://public-inbox.org/git/317468c6-40cc-9f26-8ee3-3392c3908efb@talktalk.net/T
                 # In our case, this can happen when git version invoked by git-machete to start the rebase
@@ -659,20 +787,20 @@ class GitContext:
 
         hook_path = self.get_hook_path("machete-pre-rebase")
         if self.check_hook_executable(hook_path):
-            debug(f"rebase({onto}, {fork_commit}, {branch})", f"running machete-pre-rebase hook ({hook_path})")
-            exit_code = utils.run_cmd(hook_path, onto, fork_commit, branch, cwd=self.get_root_dir())
+            debug(f"rebase({onto}, {fork_revision}, {branch})", 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()
             else:
-                sys.stderr.write("The machete-pre-rebase hook refused to rebase.\n")
-                sys.exit(exit_code)
+                raise MacheteException(
+                    f"The machete-pre-rebase hook refused to rebase. Error code: {exit_code}\n")
         else:
             do_rebase()
 
-    def rebase_onto_ancestor_commit(self, branch: str, ancestor_commit: str) -> None:
-        self.rebase(ancestor_commit, ancestor_commit, branch)
+    def rebase_onto_ancestor_commit(self, branch: LocalBranchShortName, ancestor_revision: AnyRevision, opt_no_interactive_rebase: bool) -> None:
+        self.rebase(ancestor_revision, ancestor_revision, branch, opt_no_interactive_rebase)
 
-    def get_commits_between(self, earliest_exclusive: str, latest_inclusive: str) -> List[Tuple[str, str, str]]:
+    def get_commits_between(self, earliest_exclusive: AnyRevision, latest_inclusive: AnyRevision) -> List[Tuple[str, str, str]]:
         # Reverse the list, since `git log` by default returns the commits from the latest to earliest.
         return list(reversed(list(map(
             lambda x: tuple(x.split(":", 2)),  # type: ignore
@@ -680,32 +808,32 @@ class GitContext:
                 self._popen_git("log", "--format=%H:%h:%s", f"^{earliest_exclusive}", latest_inclusive, "--"))
         ))))
 
-    def get_relation_to_remote_counterpart(self, branch: str, remote_branch: str) -> int:
-        b_is_anc_of_rb = self.is_ancestor_or_equal(branch, remote_branch, later_prefix="refs/remotes/")
-        rb_is_anc_of_b = self.is_ancestor_or_equal(remote_branch, branch, earlier_prefix="refs/remotes/")
+    def get_relation_to_remote_counterpart(self, branch: LocalBranchShortName, remote_branch: RemoteBranchShortName) -> int:
+        b_is_anc_of_rb = self.is_ancestor_or_equal(branch.full_name(), remote_branch.full_name())
+        rb_is_anc_of_b = self.is_ancestor_or_equal(remote_branch.full_name(), branch.full_name())
         if b_is_anc_of_rb:
-            return IN_SYNC_WITH_REMOTE if rb_is_anc_of_b else BEHIND_REMOTE
+            return SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE if rb_is_anc_of_b else SyncToRemoteStatuses.BEHIND_REMOTE
         elif rb_is_anc_of_b:
-            return AHEAD_OF_REMOTE
+            return SyncToRemoteStatuses.AHEAD_OF_REMOTE
         else:
-            b_t = self.get_committer_unix_timestamp_by_revision(branch, "refs/heads/")
-            rb_t = self.get_committer_unix_timestamp_by_revision(remote_branch, "refs/remotes/")
-            return DIVERGED_FROM_AND_OLDER_THAN_REMOTE if b_t < rb_t else DIVERGED_FROM_AND_NEWER_THAN_REMOTE
+            b_t = self.get_committer_unix_timestamp_by_revision(branch)
+            rb_t = self.get_committer_unix_timestamp_by_revision(remote_branch)
+            return SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE if b_t < rb_t else SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE
 
-    def get_strict_remote_sync_status(self, branch: str) -> Tuple[int, Optional[str]]:
+    def get_strict_remote_sync_status(self, branch: LocalBranchShortName) -> Tuple[int, Optional[str]]:
         if not self.get_remotes():
-            return NO_REMOTES, None
+            return SyncToRemoteStatuses.NO_REMOTES, None
         remote_branch = self.get_strict_counterpart_for_fetching_of_branch(branch)
         if not remote_branch:
-            return UNTRACKED, None
+            return SyncToRemoteStatuses.UNTRACKED, None
         return self.get_relation_to_remote_counterpart(branch, remote_branch), self.get_strict_remote_for_fetching_of_branch(branch)
 
-    def get_combined_remote_sync_status(self, branch: str) -> Tuple[int, Optional[str]]:
+    def get_combined_remote_sync_status(self, branch: LocalBranchShortName) -> Tuple[int, Optional[str]]:
         if not self.get_remotes():
-            return NO_REMOTES, None
+            return SyncToRemoteStatuses.NO_REMOTES, None
         remote_branch = self.get_combined_counterpart_for_fetching_of_branch(branch)
         if not remote_branch:
-            return UNTRACKED, None
+            return SyncToRemoteStatuses.UNTRACKED, None
         return self.get_relation_to_remote_counterpart(branch, remote_branch), self.get_combined_remote_for_fetching_of_branch(branch)
 
     def get_latest_checkout_timestamps(self) -> Dict[str, int]:  # TODO (#110): default dict with 0
@@ -729,10 +857,10 @@ class GitContext:
                     result[to_branch] = int(match.group(1))
         return result
 
-    def get_commit_information(self, information: str, commit: str = '') -> str:
+    def get_commit_information(self, information: str, commit: Optional[AnyRevision] = None) -> str:
         if information not in GIT_FORMAT_PATTERNS:
             raise MacheteException(
-                f"Retriving {information} from commit is not supported by project"
+                f"Retrieving {information} from commit is not supported by project"
                 " git-machete. Currently supported information are: "
                 f"{', '.join(GIT_FORMAT_PATTERNS.keys())}")
 
@@ -741,31 +869,31 @@ class GitContext:
             params.append(commit)
         return self._popen_git(*params)
 
-    def display_branch_history_from_forkpoint(self, branch: str, forkpoint: str) -> int:
-        return self._run_git("log", f"^{forkpoint}", branch)
+    def display_branch_history_from_forkpoint(self, branch: LocalBranchFullName, fork_point: FullCommitHash) -> int:
+        return self._run_git("log", f"^{fork_point}", branch)
 
-    def squash_commits_with_msg_and_new_env(
-            self, fork_commit: str, msg: str, env: Dict[str, str]) -> str:
+    def commit_tree_with_given_parent_and_message_and_env(
+            self, parent_revision: AnyRevision, msg: str, env: Dict[str, str]) -> FullCommitHash:
         # returns hash of the new commit
-        return self._popen_git(
-            "commit-tree", "HEAD^{tree}", "-p", fork_commit, "-m", msg, env=env)
+        return FullCommitHash.of(self._popen_git(
+            "commit-tree", "HEAD^{tree}", "-p", parent_revision, "-m", msg, env=env))
 
-    def delete_branch(self, branch_name: str, force: bool = False) -> int:
+    def delete_branch(self, branch_name: LocalBranchShortName, force: bool = False) -> int:
         self.flush_caches()
         delete_option = '-D' if force else '-d'
         return self._run_git("branch", delete_option, branch_name)
 
-    def display_diff(self, forkpoint: str, format_with_stat: bool, branch: str = None) -> int:
+    def display_diff(self, fork_point: AnyRevision, format_with_stat: bool, branch: LocalBranchShortName = None) -> int:
         params = ["diff"]
         if format_with_stat:
             params.append("--stat")
-        params.append(forkpoint)
+        params.append(fork_point)
         if branch:
-            params.append(f"refs/heads/{branch}")
+            params.append(branch.full_name())
         params.append("--")
 
         return self._run_git(*params)
 
-    def update_head_ref_to_new_hash_with_msg(self, hash: str, msg: str) -> int:
+    def update_head_ref_to_new_hash_with_reflog_subject(self, hash: FullCommitHash, reflog_subject: str) -> int:
         self.flush_caches()
-        return self._run_git("update-ref", "HEAD", hash, "-m", msg)
+        return self._run_git("update-ref", "HEAD", hash, "-m", reflog_subject)
diff --git a/git_machete/github.py b/git_machete/github.py
index 80d3785..6772f61 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -7,13 +7,13 @@ import re
 import shutil
 import subprocess
 from pathlib import Path
-from typing import Dict, List, Optional, Any, Tuple
+from typing import Any, Dict, List, Optional, Tuple
 import urllib.request
 from urllib.error import HTTPError
 
 from git_machete.utils import debug, fmt, find_or_none
 from git_machete.exceptions import MacheteException, UnprocessableEntityHTTPError
-from git_machete.git_operations import GitContext
+from git_machete.git_operations import GitContext, LocalBranchShortName
 
 
 GITHUB_TOKEN_ENV_VAR = 'GITHUB_TOKEN'
@@ -109,7 +109,20 @@ def __get_github_token() -> Optional[str]:
     def get_token_from_env() -> Optional[str]:
         return os.environ.get(GITHUB_TOKEN_ENV_VAR)
 
-    return get_token_from_env() or get_token_from_gh() or get_token_from_hub()
+    def get_token_from_file_in_home_directory() -> Optional[str]:
+        required_file_name = '.github-token'
+        file_full_path = os.path.expanduser(f'~/{required_file_name}')
+
+        if os.path.isfile(file_full_path):
+            with open(file_full_path) as file:
+                return file.read().strip()
+
+        return None
+
+    return (get_token_from_env() or
+            get_token_from_file_in_home_directory() or
+            get_token_from_gh() or
+            get_token_from_hub())
 
 
 def __fire_github_api_request(method: str, path: str, token: Optional[str], request_body: Optional[Dict[str, Any]] = None) -> Any:
@@ -134,7 +147,9 @@ def __fire_github_api_request(method: str, path: str, token: Optional[str], requ
             return parsed_response_body
     except HTTPError as err:
         if err.code == http.HTTPStatus.UNPROCESSABLE_ENTITY:
-            raise UnprocessableEntityHTTPError(str(err.reason))
+            error_response = json.loads(err.read().decode())
+            error_reason: str = error_response['errors'][0]['message']
+            raise UnprocessableEntityHTTPError(error_reason)
         elif err.code in (http.HTTPStatus.UNAUTHORIZED, http.HTTPStatus.FORBIDDEN):
             first_line = f'GitHub API returned {err.code} HTTP status with error message: `{err.reason}`\n'
             if token:
@@ -147,7 +162,8 @@ def __fire_github_api_request(method: str, path: str, token: Optional[str], requ
                                  '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')  # TODO (#164): make dedicated exception here
+                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')
             raise MacheteException(first_line + "Please open an issue regarding this topic under link: https://github.com/VirtusLab/git-machete/issues/new")
@@ -190,7 +206,7 @@ def add_reviewers_to_pull_request(org: str, repo: str, number: int, reviewers: L
     __fire_github_api_request('POST', f'/repos/{org}/{repo}/pulls/{number}/requested_reviewers', token, request_body)
 
 
-def set_base_of_pull_request(org: str, repo: str, number: int, base: str) -> None:
+def set_base_of_pull_request(org: str, repo: str, number: int, base: LocalBranchShortName) -> None:
     token: Optional[str] = __get_github_token()
     request_body: Dict[str, str] = {'base': base}
     __fire_github_api_request('PATCH', f'/repos/{org}/{repo}/pulls/{number}', token, request_body)
@@ -203,7 +219,7 @@ def set_milestone_of_pull_request(org: str, repo: str, number: int, milestone: s
     __fire_github_api_request('PATCH', f'/repos/{org}/{repo}/issues/{number}', token, request_body)
 
 
-def derive_pull_request_by_head(org: str, repo: str, head: str) -> Optional[GitHubPullRequest]:
+def derive_pull_request_by_head(org: str, repo: str, head: LocalBranchShortName) -> Optional[GitHubPullRequest]:
     token: Optional[str] = __get_github_token()
     prs = __fire_github_api_request('GET', f'/repos/{org}/{repo}/pulls?head={org}:{head}', token)
     if len(prs) >= 1:
@@ -248,6 +264,6 @@ def get_pull_request_by_number_or_none(number: int, org: str, repo: str) -> Opti
         return None
 
 
-def checkout_pr_refs(git: GitContext, remote: str, pr_number: int, branch: str) -> None:
+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)
diff --git a/git_machete/options.py b/git_machete/options.py
index 90540ed..e8afde8 100644
--- a/git_machete/options.py
+++ b/git_machete/options.py
@@ -1,20 +1,21 @@
 from typing import Optional, List
 
 from git_machete.exceptions import MacheteException
+from git_machete.git_operations import AnyRevision, LocalBranchShortName
 
 
 class CommandLineOptions:
 
     def __init__(self) -> None:
         self.opt_as_root: bool = False
-        self.opt_branch: Optional[str] = None
+        self.opt_branch: Optional[LocalBranchShortName] = None
         self.opt_checked_out_since: Optional[str] = None
         self.opt_color: str = "auto"
         self.opt_debug: bool = False
-        self.opt_down_fork_point: Optional[str] = None
+        self.opt_down_fork_point: Optional[AnyRevision] = None
         self.opt_draft: bool = False
         self.opt_fetch: bool = False
-        self.opt_fork_point: Optional[str] = None
+        self.opt_fork_point: Optional[AnyRevision] = None
         self.opt_inferred: bool = False
         self.opt_list_commits: bool = False
         self.opt_list_commits_with_hashes: bool = False
@@ -23,14 +24,14 @@ class CommandLineOptions:
         self.opt_no_detect_squash_merges: bool = False
         self.opt_no_edit_merge: bool = False
         self.opt_no_interactive_rebase: bool = False
-        self.opt_onto: Optional[str] = None
+        self.opt_onto: Optional[LocalBranchShortName] = None
         self.opt_override_to: Optional[str] = None
         self.opt_override_to_inferred: bool = False
         self.opt_override_to_parent: bool = False
         self.opt_push_tracked: Optional[bool] = True
         self.opt_push_untracked: Optional[bool] = True
         self.opt_return_to: str = "stay"
-        self.opt_roots: List[str] = list()
+        self.opt_roots: List[LocalBranchShortName] = list()
         self.opt_start_from: str = "here"
         self.opt_stat: bool = False
         self.opt_sync_github_prs: bool = False
diff --git a/git_machete/utils.py b/git_machete/utils.py
index 1892fae..fd16e2b 100644
--- a/git_machete/utils.py
+++ b/git_machete/utils.py
@@ -47,7 +47,7 @@ def tupled(f: Callable[..., T]) -> Callable[[Any], T]:
     return lambda tple: f(*tple)
 
 
-def get_second(pair: Tuple[str, str]) -> str:
+def get_second(pair: Tuple[Any, T]) -> T:
     a, b = pair
     return b
 


Test patch:
diff --git a/git_machete/tests/functional/test_machete.py b/git_machete/tests/functional/test_machete.py
index 0aa33e5..36165d7 100644
--- a/git_machete/tests/functional/test_machete.py
+++ b/git_machete/tests/functional/test_machete.py
@@ -18,33 +18,46 @@ from urllib.error import HTTPError
 
 from git_machete import cli
 from git_machete.client import MacheteClient
+from git_machete.docs import long_docs
 from git_machete.exceptions import MacheteException
 from git_machete.github import get_parsed_github_remote_url
-from git_machete.git_operations import GitContext
+from git_machete.git_operations import FullCommitHash, GitContext, LocalBranchShortName
 from git_machete.options import CommandLineOptions
 from git_machete.utils import dim, fmt
 
+
 cli_opts: CommandLineOptions = CommandLineOptions()
-git: GitContext = GitContext(cli_opts)
+git: GitContext = GitContext()
 
 FAKE_GITHUB_REMOTE_PATTERNS = ['(.*)/(.*)']
 
 
-def get_head_commit_hash() -> str:
+def get_current_commit_hash() -> FullCommitHash:
     """Returns hash of a commit of the current branch head."""
     with os.popen("git rev-parse HEAD") as git_call:
-        return git_call.read().strip()
+        return FullCommitHash.of(git_call.read().strip())
 
 
 def mock_fetch_ref(cls: Any, remote: str, ref: str) -> None:
-    branch: str = ref[ref.index(':') + 1:]
-    git.create_branch(branch, get_head_commit_hash())
+    branch: LocalBranchShortName = LocalBranchShortName.of(ref[ref.index(':') + 1:])
+    git.create_branch(branch, get_current_commit_hash())
     git.checkout(branch)
 
 
 def mock_run_cmd(cmd: str, *args: str, **kwargs: Any) -> int:
-    completed_process: subprocess.CompletedProcess[bytes] = subprocess.run([cmd] + list(args), stdout=subprocess.PIPE,
-                                                                           stderr=subprocess.PIPE, **kwargs)
+    completed_process: subprocess.CompletedProcess[bytes] = subprocess.run(
+        [cmd] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
+    exit_code: int = completed_process.returncode
+
+    if exit_code != 0:
+        sys.stderr.write(dim(f"<exit code: {exit_code}>\n\n"))
+    return completed_process.returncode
+
+
+def mock_run_cmd_and_forward_stdout(cmd: str, *args: str, **kwargs: Any) -> int:
+    completed_process: subprocess.CompletedProcess[bytes] = subprocess.run(
+        [cmd] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, ** kwargs)
+    sys.stdout.write(completed_process.stdout.decode('utf-8'))
     exit_code: int = completed_process.returncode
     if exit_code != 0:
         sys.stderr.write(dim(f"<exit code: {exit_code}>\n\n"))
@@ -55,6 +68,10 @@ def mock_derive_current_user_login() -> str:
     return "very_complex_user_token"
 
 
+def mock_ask_if(*args: str, **kwargs: Any) -> str:
+    return 'y'
+
+
 class FakeCommandLineOptions(CommandLineOptions):
     def __init__(self) -> None:
         super().__init__()
@@ -426,7 +443,7 @@ class MacheteTester(unittest.TestCase):
         self.repo_sandbox.new_branch("root")
         self.rewrite_definition_file(body)
 
-        machete_client = MacheteClient(cli_opts, git)  # Only to workaround sys.exit while calling launch(['status'])
+        machete_client = MacheteClient(git)  # Only to workaround sys.exit while calling launch(['status'])
         try:
             machete_client.read_definition_file()
         except MacheteException as e:
@@ -929,20 +946,29 @@ class MacheteTester(unittest.TestCase):
 
     @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
     def test_help(self) -> None:
-        self.launch_command("help")
-        for (description, commands) in cli.command_groups:
-            for command in commands:
-                self.launch_command("help", command)
+        expected_exit_code = None
 
-                if command not in {"format", "hooks"}:
-                    try:
-                        self.launch_command(command, "--help")
-                    except SystemExit as e:
-                        self.assertIs(e.code, 0)
-                    except Exception as e:
-                        self.fail(f'Unexpected exception raised: {e}')
-                    else:
-                        self.fail('SystemExit expected but not raised')
+        with self.assertRaises(SystemExit) as e:
+            self.launch_command("help")
+        self.assertEqual(
+            expected_exit_code, e.exception.code,
+            msg="Verify that `git machete help` causes SystemExit with "
+                f"{expected_exit_code} exit code.")
+
+        for command in long_docs:
+            with self.assertRaises(SystemExit) as e:
+                self.launch_command("help", command)
+            self.assertEqual(
+                expected_exit_code, e.exception.code,
+                msg=f"Verify that `git machete help {command}` causes SystemExit"
+                    f" with {expected_exit_code} exit code.")
+
+            with self.assertRaises(SystemExit) as e:
+                self.launch_command(command, "--help")
+            self.assertEqual(
+                expected_exit_code, e.exception.code,
+                msg=f"Verify that `git machete {command} --help` causes "
+                    f"SystemExit with {expected_exit_code} exit code.")
 
     @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
     def test_go_up(self) -> None:
@@ -1660,12 +1686,12 @@ class MacheteTester(unittest.TestCase):
             .check_out("level-1-branch")
         )
         self.launch_command("discover", "-y")
-        level_1_commit_hash = get_head_commit_hash()
+        level_1_commit_hash = get_current_commit_hash()
 
         self.repo_sandbox.check_out("root")
         self.launch_command("advance", "-y")
 
-        root_top_commit_hash = get_head_commit_hash()
+        root_top_commit_hash = get_current_commit_hash()
 
         self.assertEqual(
             level_1_commit_hash,
@@ -1727,7 +1753,7 @@ class MacheteTester(unittest.TestCase):
         )
         self.launch_command("discover", "-y")
 
-        parents_new_commit_hash = get_head_commit_hash()
+        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()
@@ -1757,13 +1783,13 @@ class MacheteTester(unittest.TestCase):
             .commit(branchs_first_commit_msg)
             .commit(branchs_second_commit_msg)
         )
-        branch_second_commit_hash = get_head_commit_hash()
+        branch_second_commit_hash = get_current_commit_hash()
         (
             self.repo_sandbox.commit("Third commit on branch.")
             .check_out("root")
             .commit("Second commit on root.")
         )
-        roots_second_commit_hash = get_head_commit_hash()
+        roots_second_commit_hash = get_current_commit_hash()
         self.repo_sandbox.check_out("branch-1")
         self.launch_command("discover", "-y")
 
@@ -1903,6 +1929,7 @@ class MacheteTester(unittest.TestCase):
 
     @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
     @mock.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+    @mock.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
     @mock.patch('urllib.request.urlopen', MockContextManager)
     @mock.patch('urllib.request.Request', git_api_state_for_test_create_pr.new_request())
     def test_github_create_pr(self) -> None:
@@ -2032,11 +2059,11 @@ class MacheteTester(unittest.TestCase):
             """,
         )
         # check against attempt to create already existing pull request
-        machete_client = MacheteClient(cli_opts, git)
+        machete_client = MacheteClient(git)
         expected_error_message = "Pull request for branch hotfix/add-trigger is already created under link www.github.com!\nPR details: PR #6 by github_user: hotfix/add-trigger -> master"
         machete_client.read_definition_file()
         with self.assertRaises(MacheteException) as e:
-            machete_client.create_github_pr('hotfix/add-trigger', draft=False)
+            machete_client.create_github_pr(head=LocalBranchShortName.of('hotfix/add-trigger'), opt_draft=False, opt_onto=None)
         if e:
             self.assertEqual(e.exception.parameter, expected_error_message,
                              'Verify that expected error message has appeared when given pull request to create is already created.')
@@ -2049,11 +2076,11 @@ class MacheteTester(unittest.TestCase):
         )
         self.launch_command('discover')
 
-        machete_client = MacheteClient(cli_opts, git)
+        machete_client = MacheteClient(git)
         machete_client.read_definition_file()
-        expected_error_message = "All commits in `testing/endpoints` branch  are already included in `develop` branch.\nCannot create pull request."
+        expected_error_message = "All commits in `testing/endpoints` branch are already included in `develop` branch.\nCannot create pull request."
         with self.assertRaises(MacheteException) as e:
-            machete_client.create_github_pr('testing/endpoints', draft=False)
+            machete_client.create_github_pr(head=LocalBranchShortName.of('testing/endpoints'), opt_draft=False, opt_onto=None)
         if e:
             self.assertEqual(e.exception.parameter, expected_error_message,
                              'Verify that expected error message has appeared when head branch is equal or ancestor of base branch.')
@@ -2061,7 +2088,7 @@ class MacheteTester(unittest.TestCase):
         self.repo_sandbox.check_out('develop')
         expected_error_message = "Branch `develop` does not have a parent branch (it is a root), base branch for the PR cannot be established."
         with self.assertRaises(MacheteException) as e:
-            machete_client.create_github_pr('develop', draft=False)
+            machete_client.create_github_pr(head=LocalBranchShortName.of('develop'), opt_draft=False, opt_onto=None)
         if e:
             self.assertEqual(e.exception.parameter, expected_error_message,
                              'Verify that expected error message has appeared when creating PR from root branch.')
@@ -2232,7 +2259,7 @@ class MacheteTester(unittest.TestCase):
             """
         )
         # check against wrong pr number
-        machete_client = MacheteClient(cli_opts, git)
+        machete_client = MacheteClient(git)
         repo: str
         org: str
         (org, repo) = get_parsed_github_remote_url(self.repo_sandbox.remote_path)
@@ -2258,7 +2285,7 @@ class MacheteTester(unittest.TestCase):
          )
         os.chdir(self.repo_sandbox.local_path)
 
-        machete_client = MacheteClient(cli_opts, git)
+        machete_client = MacheteClient(git)
         machete_client.read_definition_file()
         expected_error_message = "Could not check out PR #5 because its head branch `bugfix/remove-n-option` is already deleted from `testing`."
         with self.assertRaises(MacheteException) as e:
@@ -2276,7 +2303,8 @@ class MacheteTester(unittest.TestCase):
          )
         os.chdir(self.repo_sandbox.local_path)
 
-        expected_msg = ("Fetching testing...\n"
+        expected_msg = ("Fetching origin...\n"
+                        "Fetching testing...\n"
                         "Warn: Pull request #5 is already closed.\n"
                         "A local branch `bugfix/remove-n-option` does not exist, but a remote branch `testing/bugfix/remove-n-option` exists.\n"
                         "Checking out `bugfix/remove-n-option` locally...\n"
@@ -2321,7 +2349,7 @@ class MacheteTester(unittest.TestCase):
             .new_branch("master")
             .commit("Master commit")
             .push()
-            .delete_branch('root')
+            .delete_branch("root")
             .push()
         )
         for branch in ('develop', 'chore/sync_to_docs', 'improve/refactor', 'comments/add_docstrings'):
@@ -2349,7 +2377,8 @@ class MacheteTester(unittest.TestCase):
         )
         os.chdir(self.repo_sandbox.local_path)
         self.rewrite_definition_file("master")
-        expected_msg = ("A local branch `chore/sync_to_docs` does not exist, but a remote branch `origin/chore/sync_to_docs` exists.\n"
+        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"
@@ -2357,7 +2386,7 @@ class MacheteTester(unittest.TestCase):
                         "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"
+                        "Annotating `comments/add_docstrings` as `PR #2 (github_user)`\nAnnotating `improve/refactor` as `PR #1 (github_user)`\n"
                         "Pull request `#2` checked out at local branch `comments/add_docstrings`\n")
         self.assert_command(
             ['github', 'checkout-prs', '2'],
@@ -2380,7 +2409,8 @@ class MacheteTester(unittest.TestCase):
 
         # Check against closed pull request
         self.repo_sandbox.execute('git branch -D sphinx_export')
-        expected_msg = ("Warn: Pull request #23 is already closed.\n"
+        expected_msg = ("Fetching origin...\n"
+                        "Warn: Pull request #23 is already closed.\n"
                         "A local branch `sphinx_export` does not exist, but a remote branch `origin/sphinx_export` exists.\n"
                         "Checking out `sphinx_export` locally...\n"
                         "Added branch `sphinx_export` onto `comments/add_docstrings`\n"
@@ -2428,7 +2458,8 @@ class MacheteTester(unittest.TestCase):
             .push()
         )
         self.launch_command('discover')
-        expected_msg = ("Warn: Pull request #2 comes from fork and its repository is already deleted. No remote tracking data will be set up for `feature/allow_checkout` branch.\n"
+        expected_msg = ("Fetching origin...\n"
+                        "Warn: Pull request #2 comes from fork and its repository is already deleted. No remote tracking data will be set up for `feature/allow_checkout` branch.\n"
                         "Checking out `feature/allow_checkout` locally...\n"
                         "Warn: Pull request #2 is already closed.\n"
                         "Added branch `feature/allow_checkout` onto `develop`\n"
@@ -2445,3 +2476,202 @@ class MacheteTester(unittest.TestCase):
             msg="Verify that 'git machete github checkout prs' performs 'git checkout' to "
                 "the head branch of given pull request."
         )
+
+    def test_squash_with_valid_fork_point(self) -> None:
+        (
+            self.repo_sandbox.new_branch('branch-0')
+                .commit("First commit.")
+                .commit("Second commit.")
+        )
+        fork_point = get_current_commit_hash()
+
+        (
+            self.repo_sandbox.commit("Third commit.")
+                .commit("Fourth commit.")
+        )
+
+        self.launch_command('squash', '-f', fork_point)
+
+        current_branch_log = os.popen('git log -3 --format=%s').read()
+
+        expected_branch_log = (
+            "Third commit.\n"
+            "Second commit.\n"
+            "First commit.\n"
+        )
+
+        self.assertEqual(
+            current_branch_log,
+            expected_branch_log,
+            msg=("Verify that `git machete squash -f <fork-point>` squashes commit"
+                 " from one succeeding the fork-point until tip of the branch.")
+        )
+
+    def test_squash_with_invalid_fork_point(self) -> None:
+        (
+            self.repo_sandbox.new_branch('branch-0')
+                .commit()
+                .new_branch('branch-1a')
+                .commit()
+        )
+        fork_point_to_branch_1a = get_current_commit_hash()
+
+        (
+            self.repo_sandbox.check_out('branch-0')
+                .new_branch('branch-1b')
+                .commit()
+        )
+
+        with self.assertRaises(SystemExit):
+            # First exception MacheteException is raised, followed by SystemExit.
+            self.launch_command('squash', '-f', fork_point_to_branch_1a)
+
+    def test_update_with_invalid_fork_point(self) -> None:
+        (
+            self.repo_sandbox.new_branch('branch-0')
+                .commit("Commit on branch-0.")
+                .new_branch("branch-1a")
+                .commit("Commit on branch-1a.")
+        )
+        branch_1a_hash = get_current_commit_hash()
+        (
+            self.repo_sandbox.check_out('branch-0')
+                .new_branch("branch-1b")
+                .commit("Commit on branch-1b.")
+        )
+
+        self.launch_command('discover', '-y')
+
+        with self.assertRaises(SystemExit):
+            # First exception MacheteException is raised, followed by SystemExit.
+            self.launch_command('update', '-f', branch_1a_hash)
+
+    def test_slide_out_with_valid_down_fork_point(self) -> None:
+        (
+            self.repo_sandbox.new_branch('branch-0')
+                .commit()
+                .new_branch('branch-1')
+                .commit()
+                .new_branch('branch-2')
+                .commit()
+                .new_branch('branch-3')
+                .commit()
+                .commit('Second commit on branch-3.')
+        )
+        hash_of_second_commit_on_branch_3 = get_current_commit_hash()
+        self.repo_sandbox.commit("Third commit on branch-3.")
+
+        self.launch_command('discover', '-y')
+        self.launch_command(
+            'slide-out', '-n', 'branch-1', 'branch-2', '-d',
+            hash_of_second_commit_on_branch_3)
+
+        expected_status_output = (
+            """
+            branch-0 (untracked)
+            |
+            | Third commit on branch-3.
+            o-branch-3 * (untracked)
+            """
+        )
+
+        self.assert_command(['status', '-l'], expected_status_output)
+
+    def test_slide_out_with_invalid_down_fork_point(self) -> None:
+        (
+            self.repo_sandbox.new_branch('branch-0')
+                .commit()
+                .new_branch('branch-1')
+                .commit()
+                .new_branch('branch-2')
+                .commit()
+                .new_branch('branch-3')
+                .commit()
+                .check_out('branch-2')
+                .commit('Commit that is not ancestor of branch-3.')
+        )
+        hash_of_commit_that_is_not_ancestor_of_branch_2 = get_current_commit_hash()
+
+        self.launch_command('discover', '-y')
+
+        with self.assertRaises(SystemExit):
+            self.launch_command(
+                'slide-out', '-n', 'branch-1', 'branch-2', '-d',
+                hash_of_commit_that_is_not_ancestor_of_branch_2)
+
+    def test_slide_out_with_down_fork_point_and_multiple_children_of_last_branch(self) -> None:
+        (
+            self.repo_sandbox.new_branch('branch-0')
+                .commit()
+                .new_branch('branch-1')
+                .commit()
+                .new_branch('branch-2a')
+                .commit()
+                .check_out('branch-1')
+                .new_branch('branch-2b')
+                .commit()
+        )
+
+        hash_of_only_commit_on_branch_2b = get_current_commit_hash()
+
+        self.launch_command('discover', '-y')
+
+        with self.assertRaises(SystemExit):
+            self.launch_command(
+                'slide-out', '-n', 'branch-1', '-d',
+                hash_of_only_commit_on_branch_2b)
+
+    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd_and_forward_stdout)
+    def test_log(self) -> None:
+        self.repo_sandbox.new_branch('root')
+        self.repo_sandbox.commit()
+        roots_only_commit_hash = get_current_commit_hash()
+
+        self.repo_sandbox.new_branch('child')
+        self.repo_sandbox.commit()
+        childs_first_commit_hash = get_current_commit_hash()
+        self.repo_sandbox.commit()
+        childs_second_commit_hash = get_current_commit_hash()
+
+        log_content = self.launch_command('log')
+
+        self.assertIn(
+            childs_first_commit_hash, log_content,
+            msg="Verify that oldest commit from current branch is visible when "
+                "executing `git machete log`.")
+        self.assertIn(
+            childs_second_commit_hash, log_content,
+            msg="Verify that youngest commit from current branch is visible when "
+                "executing `git machete log`.")
+        self.assertNotIn(
+            roots_only_commit_hash, log_content,
+            msg="Verify that commits from parent branch are not visible when "
+                "executing `git machete log`.")
+
+    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+    def test_add(self) -> None:
+        """
+        Verify behaviour of a 'git machete add' command.
+        """
+        (
+            self.repo_sandbox.new_branch("master")
+                .commit("master commit.")
+                .new_branch("develop")
+                .commit("develop commit.")
+                .new_branch("feature")
+                .commit("feature commit.")
+                .check_out("develop")
+                .commit("New commit on develop")
+        )
+        self.launch_command("discover", "-y")
+        self.repo_sandbox.new_branch("bugfix/feature_fail")
+
+        self.assert_command(['add', '-y', 'bugfix/feature_fail'], 'Adding `bugfix/feature_fail` onto the inferred upstream (parent) branch `develop`\n'
+                                                                  'Added branch `bugfix/feature_fail` onto `develop`\n', strip_indentation=False)
+
+        # test with --onto option
+        self.repo_sandbox.new_branch("chore/remove_indentation")
+
+        self.assert_command(['add', '--onto=feature'],
+                            'Added branch `chore/remove_indentation` onto `feature`\n',
+                            strip_indentation=False)