Repository: VirtusLab/git-machete
Pull request #921 (2023-05-02 13:15:04)

Release v3.7.2


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6e58750..c7bf05a 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -60,7 +60,7 @@ jobs:
   # (other than Python 3.6 being EOLed and hence not supporting the versions of packages used for the checks).
   # Each of these needs to be run against just one Python version
   # (since there's very little chance that they pass under one version but fail under another),
-  # so let's spread these checks evenly across versions to reduce CI latency
+  # so let's spread these checks evenly across versions to reduce CI execution time
   # (when compared to the scenario where all checks are executed under the same Python version).
 
   "pep8 and python 3_8 git 2_25_0":
@@ -68,7 +68,7 @@ jobs:
     steps:
       - checkout
       - run:
-          name: Run tests against Python 3.8 and git 2.25.0 (with PEP8 check)
+          name: Run tests against Python 3.8 and git 2.25.0 (with isort/PEP8/vulture check)
           command: PYTHON_VERSION=3.8 GIT_VERSION=2.25.0 CHECK_PEP8=true  bash ci/tox/ci-run.sh
       - store_test_results:
           path: test-results/
diff --git a/.dockerignore b/.dockerignore
index 3eea7d5..d08d582 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,7 +1,6 @@
 *
 
 !git_machete/
-!git-machete
 !MANIFEST.in
 !README.md
 !requirements.txt
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 83ae421..ad88415 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,10 @@
 # Release notes
 
+## New in git-machete 3.17.2
+
+- fixed: when `origin/feature/foo` branch exists, `git machete add foo` no longer falsely recognizes `origin/feature/foo` as a potential remote tracking branch for `foo`
+- fixed: on Windows, git-machete installed globally via `pip` no longer crashes on `ModuleNotFoundError` within venvs (contributed by @cspotcode)
+
 ## New in git-machete 3.17.1
 
 - fixed: in the rare case when overridden fork point for branch X is an ancestor of X's parent, the effective fork point is selected to the latest common ancestor of X and X's parent
diff --git a/blogs/docker-ci-tricks-1/blog.md b/blogs/docker-ci-tricks-1/blog.md
index d3408da..66a6e42 100644
--- a/blogs/docker-ci-tricks-1/blog.md
+++ b/blogs/docker-ci-tricks-1/blog.md
@@ -157,9 +157,9 @@ that is `COPY`-ed into the image:
 set -e -u -x
 
 if [[ $CHECK_COVERAGE = true ]]; then
-  TOX_ENV_LIST="pep8,coverage"
+  TOX_ENV_LIST="pep8-check,coverage"
 else
-  TOX_ENV_LIST="pep8,py${PYTHON_VERSION/./}"
+  TOX_ENV_LIST="pep8-check,py${PYTHON_VERSION/./}"
 fi
 
 tox -e $TOX_ENV_LIST
diff --git a/ci/ci-run-commons.sh b/ci/ci-run-commons.sh
index 1c0914e..2bd4bb4 100644
--- a/ci/ci-run-commons.sh
+++ b/ci/ci-run-commons.sh
@@ -19,24 +19,24 @@ function retry() {
 # If image is not found by pull, build the image and push it to the Docker Hub.
 function docker_compose_pull_or_build_and_push() {
   image_name=$1
-  DIRECTORY_HASH=$(git rev-parse HEAD:ci/$image_name)
+  DIRECTORY_HASH=$(git rev-parse "HEAD:ci/$image_name")
   export DIRECTORY_HASH
   cd "$(git rev-parse --show-toplevel)/ci/$image_name/"
 
   # Let's retry pulling the image in case of a spurious failure
   # (`error pulling image configuration: Get "https://docker-images-prod.s3.dualstack.us-east-1.amazonaws.com/...": dial tcp ...:443: i/o timeout`)
-  retry 3 docker-compose --ansi=never pull $image_name
+  retry 3 docker-compose --ansi=never pull "$image_name"
 
   # A very unpleasant workaround for https://github.com/docker/compose/issues/7258
   # (since v1.25.1, `docker-compose pull` is NOT failing when it can't fetch the image).
   image_tag=$(docker-compose --ansi=never config | yq eval ".services.$image_name.image" -)
   if ! docker image inspect "$image_tag" &>/dev/null; then
-    docker-compose --ansi=never build $image_name
+    docker-compose --ansi=never build --progress=plain "$image_name"
     # In builds coming from forks, secret vars are unavailable for security reasons; hence, we have to skip pushing the newly built image.
     if [[ ${DOCKER_PASSWORD-} && ${DOCKER_USERNAME-} ]]; then
       echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
       # In case the push fails due to e.g. timeouts (which unfortunately sometimes happen on CI), we don't want to fail the entire deployment.
-      docker-compose --ansi=never push $image_name || true
+      docker-compose --ansi=never push "$image_name" || true
     fi
   fi
 }
diff --git a/ci/deb-ppa-upload/local-run.sh b/ci/deb-ppa-upload/local-run.sh
index 6dd6571..02a35b3 100755
--- a/ci/deb-ppa-upload/local-run.sh
+++ b/ci/deb-ppa-upload/local-run.sh
@@ -9,7 +9,7 @@ cd "$(git rev-parse --show-toplevel)"/ci/deb-ppa-upload/
 check_env gpg-ssh.env
 
 set -x
-docker-compose build deb-ppa-upload
+docker-compose --progress=plain build deb-ppa-upload
 common_flags=(
   -e "GID=$(id -g)"
   -e "UID=$(id -u)"
diff --git a/ci/docker/ci-deploy.sh b/ci/docker/ci-deploy.sh
index 9f343bd..aad842c 100755
--- a/ci/docker/ci-deploy.sh
+++ b/ci/docker/ci-deploy.sh
@@ -15,7 +15,7 @@ docker build \
   -t gitmachete/git-machete:latest \
   -f ci/docker/Dockerfile .
 
-[[ $(docker run gitmachete/git-machete:latest --version) == "git machete version $version" ]]
+[[ $(docker run gitmachete/git-machete:latest version) == "git-machete version $version" ]]
 
 if [[ $do_push == true ]]; then
   echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
diff --git a/ci/nixpkgs-build/local-run.sh b/ci/nixpkgs-build/local-run.sh
index 01aa449..75552fe 100755
--- a/ci/nixpkgs-build/local-run.sh
+++ b/ci/nixpkgs-build/local-run.sh
@@ -8,5 +8,5 @@ export_directory_hash nixpkgs-build
 cd "$(git rev-parse --show-toplevel)"/ci/nixpkgs-build/
 
 git_revision=$(git rev-parse "@{upstream}")
-docker-compose build nixpkgs-build
+docker-compose --progress=plain build nixpkgs-build
 docker-compose run -e GIT_REVISION="$git_revision" nixpkgs-build
diff --git a/ci/rpm/local-run.sh b/ci/rpm/local-run.sh
index f3abd4f..37c0ffb 100755
--- a/ci/rpm/local-run.sh
+++ b/ci/rpm/local-run.sh
@@ -8,5 +8,5 @@ export_directory_hash rpm
 cd "$(git rev-parse --show-toplevel)"/ci/rpm/
 
 set -x
-docker-compose build rpm
+docker-compose --progress=plain build rpm
 docker-compose run -e GID="$(id -g)" -e UID="$(id -u)" rpm
diff --git a/ci/tox/build-context/run.sh b/ci/tox/build-context/run.sh
index 070d930..a786d87 100644
--- a/ci/tox/build-context/run.sh
+++ b/ci/tox/build-context/run.sh
@@ -22,11 +22,11 @@ if [[ $BUILD_SPHINX_HTML = true ]]; then
 fi
 
 if [[ $CHECK_DOCS_UP_TO_DATE = true ]]; then
-  TOX_ENV_LIST="$TOX_ENV_LIST,check-py-docs,check-sphinx-man"
+  TOX_ENV_LIST="$TOX_ENV_LIST,py-docs-check,sphinx-man-check"
 fi
 
 if [[ $CHECK_PEP8 = true ]]; then
-  TOX_ENV_LIST="$TOX_ENV_LIST,pep8,isort-check"
+  TOX_ENV_LIST="$TOX_ENV_LIST,isort-check,pep8-check,vulture-check"
 fi
 
 tox -e "$TOX_ENV_LIST"
diff --git a/ci/tox/local-run.sh b/ci/tox/local-run.sh
index e0702f8..7b13a05 100755
--- a/ci/tox/local-run.sh
+++ b/ci/tox/local-run.sh
@@ -10,5 +10,5 @@ check_var GIT_VERSION
 check_var PYTHON_VERSION
 
 set -x
-docker-compose build tox
+docker-compose --progress=plain build tox
 docker-compose run -e GID="$(id -g)" -e UID="$(id -u)" tox
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 5401a85..200cf12 100644
--- a/docs/man/git-machete.1
+++ b/docs/man/git-machete.1
@@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "GIT-MACHETE" "1" "Apr 18, 2023" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Apr 25, 2023" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.17.1
+git-machete \- git-machete 3.17.2
 .sp
 Probably the sharpest git repository organizer & rebase/merge workflow automation tool you\(aqve ever seen.
 .sp
diff --git a/git-machete b/git-machete
deleted file mode 100644
index 9e063a4..0000000
--- a/git-machete
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/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 075ff0f..b486aa5 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.17.1'
+__version__ = '3.17.2'
diff --git a/git_machete/annotation.py b/git_machete/annotation.py
index 28880b9..7378f0a 100644
--- a/git_machete/annotation.py
+++ b/git_machete/annotation.py
@@ -8,7 +8,6 @@ class Qualifiers:
     push: bool
 
     def __init__(self, annotation: str):
-        self._annotation: str = annotation
         self._annotation_without_qualifiers: str = annotation
         self._rebase_text: str = ''
         self._push_text: str = ''
diff --git a/git_machete/bin.py b/git_machete/bin.py
new file mode 100644
index 0000000..5f6d780
--- /dev/null
+++ b/git_machete/bin.py
@@ -0,0 +1,21 @@
+import sys
+
+# Since this shim needs to be compatible with Python 2,
+# let's skip mypy checks, as type annotations were only introduced in Python 3.5.
+
+
+def main():  # type: ignore
+    def validate_python_version():  # type: ignore
+        if sys.version_info[:2] < (3, 6):
+            # String interpolations were only introduced in Python 3.6
+            version_str = "{}.{}.{}".format(sys.version_info.major, sys.version_info.minor, sys.version_info.micro)  # noqa: FS002
+            sys.stderr.write(
+                "Python {} is no longer supported. \n".format(version_str) +  # noqa: FS002
+                "Please switch to Python 3.6 or higher.\n")
+            sys.exit(1)
+
+    validate_python_version()  # type: ignore
+
+    from . import cli
+
+    cli.main()
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 164765c..79305d2 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -19,8 +19,6 @@ from git_machete.utils import bold, excluding, fmt, underline, warn
 
 T = TypeVar('T')
 
-initial_current_directory: Optional[str] = utils.get_current_directory_or_none() or os.getenv('PWD')
-
 alias_by_command: Dict[str, str] = {
     "diff": "d",
     "edit": "e",
@@ -47,21 +45,17 @@ command_groups: List[Tuple[str, List[str]]] = [
      ["github"])
 ]
 
-commands_and_aliases = list(long_docs.keys()) + list(alias_by_command.keys())
+commands_and_aliases = list(long_docs.keys()) + list(command_by_alias.keys())
 
 
 def get_help_description(display_help_topics: bool, command: Optional[str] = None) -> str:
     usage_str = ''
-    if command and command in commands_and_aliases:
-        if command in long_docs:
-            usage_str += fmt(textwrap.dedent(long_docs[command]))
-        elif command in command_by_alias:
-            usage_str += fmt(textwrap.dedent(long_docs[command_by_alias[command]]))
+    if command in long_docs:
+        usage_str += fmt(textwrap.dedent(long_docs[command]))
+    elif command in command_by_alias:
+        usage_str += fmt(textwrap.dedent(long_docs[command_by_alias[command]]))
     else:
-        usage_str += get_short_general_usage() + '\n'
-        if command:
-            usage_str += f"\nUnknown command: '{command}'"
-        usage_str += fmt(
+        usage_str += get_short_general_usage() + '\n' + fmt(
             "\n<u>TL;DR tip</u>\n\n"
             "    Get familiar with the help for <b>format</b>, <b>edit</b>,"
             " <b>status</b> and <b>update</b>, in this order.\n\n")
@@ -112,9 +106,9 @@ class MacheteHelpAction(argparse.Action):
     def __call__(
             self,
             parser: argparse.ArgumentParser,
-            namespace: argparse.Namespace,
+            namespace: argparse.Namespace,  # noqa: F841
             values: Union[str, Sequence[Any], None],
-            option_string: Optional[str] = None
+            option_string: Optional[str] = None  # noqa: F841
     ) -> None:
         # parser name (prog) is expected to be `git machete` or `git machete <command>`
         command_name = parser.prog.replace('git machete', '').strip()
@@ -128,7 +122,7 @@ def create_cli_parser() -> argparse.ArgumentParser:
     common_args_parser.add_argument('--debug', action='store_true')
     common_args_parser.add_argument('-h', '--help', action=MacheteHelpAction)
     common_args_parser.add_argument(
-        '--version', action='version', version=f'%(prog)s version {__version__}')
+        '--version', action='version', version=f'git-machete version {__version__}')
     common_args_parser.add_argument('-v', '--verbose', action='store_true')
 
     cli_parser = argparse.ArgumentParser(
@@ -480,8 +474,7 @@ def update_cli_options_using_config_keys(
 
 def set_utils_global_variables(
         cli_opts: git_machete.options.CommandLineOptions) -> None:
-    if cli_opts.opt_color:
-        utils.ascii_only = (cli_opts.opt_color == "never" or (cli_opts.opt_color == "auto" and not sys.stdout.isatty()))
+    utils.ascii_only = (cli_opts.opt_color == "never" or (cli_opts.opt_color == "auto" and not sys.stdout.isatty()))
     utils.debug_mode = cli_opts.opt_debug
     utils.verbose_mode = cli_opts.opt_verbose
 
@@ -511,6 +504,7 @@ def exit_script(status_code: int, error: Optional[BaseException] = None) -> None
 def launch(orig_args: List[str]) -> None:
     cli_opts = git_machete.options.CommandLineOptions()
     git = GitContext()
+    initial_current_directory: Optional[str] = utils.get_current_directory_or_none()
 
     try:
         cli_parser: argparse.ArgumentParser = create_cli_parser()
@@ -735,6 +729,8 @@ def launch(orig_args: List[str]) -> None:
                     filter(
                         lambda _branch: machete_client.has_any_fork_point_override_config(_branch),
                         git.get_local_branches()))
+            else:  # should never happen
+                raise MacheteException(f"Invalid category: `{category}`")
 
             if res:
                 print("\n".join(res))
@@ -859,6 +855,10 @@ def launch(orig_args: List[str]) -> None:
     except StopInteraction:
         pass
     finally:
+        # Note that this problem (current directory no longer existing due to e.g. underlying git checkouts)
+        # seems to be specific to Linuxes (or to older versions of git).
+        # Under macOS 13.2 + git 2.40.0, the directory isn't removed during a checkout
+        # if it's a current working directory of the git process.
         if initial_current_directory and not utils.does_directory_exist(initial_current_directory):
             nearest_existing_parent_directory = initial_current_directory
             while not utils.does_directory_exist(nearest_existing_parent_directory):
diff --git a/git_machete/client.py b/git_machete/client.py
index bac958d..7544cdd 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -206,7 +206,7 @@ class MacheteClient:
 
     def render_tree(self) -> List[str]:
         if not self.__indent:
-            self.__indent = "\t"
+            self.__indent = "  "
 
         def render_dfs(branch: LocalBranchShortName, depth: int) -> List[str]:
             annotation = self.__annotations[branch].get_unformatted_text() if branch in self.__annotations else ""
@@ -384,7 +384,7 @@ class MacheteClient:
                 self.__roots += [LocalBranchShortName.of("develop")]
         self.__down_branches = {}
         self.up_branch = {}
-        self.__indent = "\t"
+        self.__indent = "  "
         for branch in self.annotations.keys():
             self.annotations[branch].text_without_qualifiers = ''
 
@@ -1132,7 +1132,7 @@ class MacheteClient:
             yellow_edge_branch: LocalBranchShortName = branches_in_sync_but_fork_point_off[0]
             if len(branches_in_sync_but_fork_point_off) == 1:
                 first_part = (f"yellow edge indicates that fork point for {bold(str(yellow_edge_branch))} "
-                              f"is probably incorrectly inferred,\n or that some extra branch should be between "
+                              f"is probably incorrectly inferred,\nor that some extra branch should be between "
                               f"{bold(str(self.up_branch[yellow_edge_branch]))} and "
                               f"{bold(str(yellow_edge_branch))}")
             else:
@@ -1176,7 +1176,7 @@ class MacheteClient:
                 msg_core_suffix = '' if is_merged_to_remote else f', but not merged to {bold(remote_branch)}'  # type: ignore[arg-type]
                 msg_core = f"{bold(branch)} (merged to HEAD{msg_core_suffix})"
                 msg = f"Delete branch {msg_core}?" + get_pretty_choices('y', 'N', 'q')
-                opt_yes_msg = f"Deleting branch {msg_core}"
+                opt_yes_msg = f"Deleting branch {msg_core}..."
                 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)
@@ -1187,7 +1187,7 @@ class MacheteClient:
             for branch in branches_to_delete_unmerged_to_head:
                 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}"
+                opt_yes_msg = f"Deleting branch {msg_core}..."
                 ans = self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes)
                 if ans in ('y', 'yes'):
                     self.__git.delete_branch(branch, force=True)
@@ -1440,8 +1440,8 @@ class MacheteClient:
                 "start of range of commits to squash.")
         if len(commits) == 1:
             print(f"Exactly one commit ({bold(commits[0].short_hash)}) to squash, ignoring.\n")
-            print("Tip: use `-f` or `--fork-point` to specify where the range of "
-                  "commits to squash starts.")
+            print(fmt("Tip: use `-f` or `--fork-point` to specify where the range of "
+                  "commits to squash starts."))
             return
 
         earliest_commit = commits[0]
diff --git a/git_machete/constants.py b/git_machete/constants.py
index 4cf5816..6064987 100644
--- a/git_machete/constants.py
+++ b/git_machete/constants.py
@@ -22,4 +22,3 @@ class GitFormatPatterns(Enum):
     AUTHOR_EMAIL = "%aE"
     AUTHOR_DATE = "%ai"
     RAW_BODY = "%B"
-    SUBJECT = "%s"
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 875b357..be88185 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -168,8 +168,8 @@ class GitReflogEntry(NamedTuple):
 
 
 class BranchPair(NamedTuple):
-    local_branch: LocalBranchShortName
-    local_or_remote_branch: AnyBranchName
+    local_branch: LocalBranchShortName  # noqa: F841
+    local_or_remote_branch: AnyBranchName  # noqa: F841
 
 
 HEAD = AnyRevision.of("HEAD")
@@ -471,9 +471,6 @@ class GitContext:
     def is_full_hash(revision: AnyRevision) -> Optional[Match[str]]:
         return re.match("^[0-9a-f]{40}$", revision)  # noqa: FS003
 
-    def is_commit_present_in_repository(self, revision: AnyRevision) -> bool:
-        return self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{commit}", allow_non_zero=True).exit_code == 0  # noqa: FS003, E501
-
     def get_committer_unix_timestamp_by_revision(self, revision: AnyBranchName) -> int:
         if self.__committer_unix_timestamp_by_revision_cached is None:
             self.__load_branches()
@@ -652,10 +649,10 @@ class GitContext:
             if len(values) != 3:  # invalid, shouldn't happen
                 continue
             selector, hash, subject = values
-            branch_and_pos = selector.split("@")
-            if len(branch_and_pos) != 2:  # invalid, shouldn't happen
+            branch_and_index = selector.split("@")
+            if len(branch_and_index) != 2:  # invalid, shouldn't happen
                 continue
-            branch, pos = branch_and_pos
+            branch, _ = branch_and_index
             any_branch_name = AnyBranchName.of(branch)
             if any_branch_name not in self.__reflogs_cached:
                 self.__reflogs_cached[any_branch_name] = []
@@ -854,16 +851,9 @@ class GitContext:
         return result
 
     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';
-            # this is to reduce risk wrt. which '/'-separated fragments belong to remote and which to branch name.
-            # FIXME (#116): this is still likely to deliver incorrect results in rare corner cases with compound remote names.
-            return remote_branch.endswith(f"/{branch}")
-
-        matching_remote_branches = list(filter(matches, self.get_remote_branches()))
-        return matching_remote_branches[0] if len(matching_remote_branches) == 1 else None
+        remote_branches = self.get_remote_branches()
+        matching_remotes = [remote for remote in self.get_remotes() if (remote + "/" + branch) in remote_branches]
+        return RemoteBranchShortName(matching_remotes[0] + "/" + branch) if len(matching_remotes) == 1 else None
 
     def get_merged_local_branches(self) -> List[LocalBranchShortName]:
         if self.get_git_version() >= (2, 7, 6):  # earliest version of git to support 'for-each-ref --merged'
diff --git a/git_machete/utils.py b/git_machete/utils.py
index b16f58d..6bab58b 100644
--- a/git_machete/utils.py
+++ b/git_machete/utils.py
@@ -49,7 +49,7 @@ def tupled(f: Callable[..., T]) -> Callable[[Any], T]:
 
 
 def get_second(pair: Tuple[Any, T]) -> T:
-    a, b = pair
+    _, b = pair
     return b
 
 
diff --git a/graphics/setup-sandbox b/graphics/setup-sandbox
index 6d9cdc9..f04b01d 100755
--- a/graphics/setup-sandbox
+++ b/graphics/setup-sandbox
@@ -21,7 +21,7 @@ newrepo() {
   mkdir $root_dir/$dir
   cd $root_dir/$dir
   opt=${2-}
-  git init $opt
+  git -c init.defaultBranch=master init $opt
 }
 
 push() {
diff --git a/mypy.ini b/mypy.ini
index 59bd045..32bad6c 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1,15 +1,15 @@
 [mypy]
-follow_imports = normal
+check_untyped_defs = True
+disallow_any_generics = True
 disallow_incomplete_defs = True
-disallow_untyped_defs = True
-strict_optional = True
+disallow_subclassing_any = True
 disallow_untyped_calls = True
 disallow_untyped_decorators = True
+disallow_untyped_defs = True
+follow_imports = normal
 no_implicit_optional = True
+strict_optional = True
+warn_redundant_casts = True
 warn_return_any = True
 warn_unused_configs = True
-disallow_subclassing_any = True
-disallow_any_generics = True
-check_untyped_defs = True
-warn_redundant_casts = True
 warn_unused_ignores = True
diff --git a/requirements.coverage.txt b/requirements.coverage.txt
index f19a05d..9c246a6 100644
--- a/requirements.coverage.txt
+++ b/requirements.coverage.txt
@@ -1,3 +1,3 @@
-coverage==7.2.3
+coverage==7.2.5
 pytest==7.3.1
 pytest-mock==3.10.0
diff --git a/requirements.pep8-check.txt b/requirements.pep8-check.txt
new file mode 100644
index 0000000..72e2b50
--- /dev/null
+++ b/requirements.pep8-check.txt
@@ -0,0 +1,2 @@
+flake8==6.0.0
+flake8-use-fstring==1.4
diff --git a/requirements.pep8.txt b/requirements.pep8.txt
index 72e2b50..30c5e1e 100644
--- a/requirements.pep8.txt
+++ b/requirements.pep8.txt
@@ -1,2 +1,2 @@
-flake8==6.0.0
-flake8-use-fstring==1.4
+autoflake==2.1.1
+autopep8==2.0.2
diff --git a/requirements.sphinx-docs.txt b/requirements.sphinx-docs.txt
index 467b94d..4bcc463 100644
--- a/requirements.sphinx-docs.txt
+++ b/requirements.sphinx-docs.txt
@@ -1,2 +1,2 @@
-sphinx==6.2.0
+sphinx==6.2.1
 sphinx-rtd-theme==1.2.0
diff --git a/requirements.vulture-check.txt b/requirements.vulture-check.txt
new file mode 100644
index 0000000..a8398d1
--- /dev/null
+++ b/requirements.vulture-check.txt
@@ -0,0 +1,1 @@
+vulture==2.7
diff --git a/setup.py b/setup.py
index f28d2a1..97f8a57 100644
--- a/setup.py
+++ b/setup.py
@@ -22,7 +22,11 @@ setup(
     license='MIT',
     keywords='git',
     packages=['git_machete'],
-    scripts=['git-machete'],
+    entry_points={
+        'console_scripts': [
+            'git-machete = git_machete.bin:main'
+        ]
+    },
     python_requires='>=3.6, <4',
     classifiers=[
         'Programming Language :: Python',
diff --git a/tox.ini b/tox.ini
index 3c02052..c5ce8c3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,6 @@
 [tox]
-envlist = pep8,mypy-py{36,37,38,39,310,311},py{36,37,38,39,310,311},coverage,sphinx-html,sphinx-man,py-docs
+# Environments selected for frequent execution during development
+envlist = pep8,isort,mypy-py311
 minversion = 2.3.2
 skipsdist = True
 
@@ -22,12 +23,21 @@ deps =
   -r{toxinidir}/requirements.testenv.py36-txt
 
 [testenv:pep8]
-description = "Check if formatting conforms to PEP 8"
+description = "Apply PEP8 formatting + remove unused imports"
 deps =
   -r{toxinidir}/requirements.pep8.txt
+# E402 - Module level import not at top of file
+commands =
+  autoflake --in-place --recursive --remove-all-unused-imports .
+  autopep8 --ignore=E402 --in-place --recursive .
+
+[testenv:pep8-check]
+description = "Check if formatting conforms to PEP 8"
+deps =
+  -r{toxinidir}/requirements.pep8-check.txt
 commands = flake8 --enable-extensions=FS003
 
-# Set flake8 configuration options which are used by the `flake8` command in [testenv:pep8]
+# Set flake8 configuration options which are used by the `flake8` command in [testenv:pep8] and [testenv:pep8-check]
 [flake8]
 # W504 ignore wrap line after binary operator
 ignore = W504
@@ -36,7 +46,7 @@ max-line-length = 140
 exclude = ./.*,build,dist,*egg,venv, git_machete/generated_docs.py
 import-order-style = pep8
 
-[testenv:isort-apply]
+[testenv:isort]
 description = "Sort the imports in Python code"
 deps =
   -r{toxinidir}/requirements.isort.txt
@@ -48,6 +58,12 @@ deps =
   -r{toxinidir}/requirements.isort.txt
 commands = isort --check-only .
 
+[testenv:vulture-check]
+description = "Run `vulture` static code analyzer to detect unused code"
+deps =
+  -r{toxinidir}/requirements.vulture-check.txt
+commands = vulture git_machete/ tests/
+
 [testenv:venv]
 commands = {posargs}
 
@@ -58,7 +74,8 @@ deps =
 commands =
   coverage erase
   # Generates .coverage binary file, used as the source of truth for the subsequent reports
-  coverage run --branch --include='git_machete/*' --omit='git_machete/tests/*' -m pytest --junitxml=test-results/testenv-{envname}.xml
+  # Note that coverage (as of v7.2.3) doesn't support parallel running (`--numprocesses=...`) via python-xdist
+  coverage run --branch --omit='tests/*' -m pytest --junitxml=test-results/testenv-{envname}.xml
   # Prints a report to console
   coverage report
   # Saves a report to htmlcov/
@@ -66,6 +83,17 @@ commands =
   # Saves a report to coverage.xml (uploaded to Codecov from CI, as Codecov apparently does not accept .coverage binary files)
   coverage xml
 
+[coverage:report]
+exclude_lines =
+  def main\(\)
+  except EOFError:
+  except KeyboardInterrupt:
+  if __name__ == .__main__.:
+  # We're slightly cheating here, as we're skipping all lines where exceptions are raised from coverage report.
+  # Still, covering them with tests has a limited value,
+  # and they divert attention from the more important happy-path logic in coverage reports.
+  ^ *raise .*$
+
 [testenv:sphinx-html]
 description = "Build Sphinx documentation in HTML"
 allowlist_externals = bash
@@ -82,7 +110,7 @@ deps =
 commands =
   bash docs/generate-sphinx-man.sh docs/man
 
-[testenv:check-sphinx-man]
+[testenv:sphinx-man-check]
 description = "Check if Unix man page is up to date with reStructuredText sources"
 allowlist_externals = bash
 deps =
@@ -99,7 +127,7 @@ deps =
 commands =
   bash -c "python docs/generate_py_docs.py > git_machete/generated_docs.py"
 
-[testenv:check-py-docs]
+[testenv:py-docs-check]
 description = "Check if Python documentation is up to date with with reStructuredText sources"
 allowlist_externals = bash
 deps =


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29..6785843 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,4 @@
+import pytest
+
+# See https://docs.pytest.org/en/latest/how-to/writing_plugins.html#assertion-rewriting
+pytest.register_assert_rewrite("tests.mockers")
diff --git a/tests/base_test.py b/tests/base_test.py
new file mode 100644
index 0000000..2ae8fc6
--- /dev/null
+++ b/tests/base_test.py
@@ -0,0 +1,136 @@
+import os
+import subprocess
+import time
+from tempfile import mkdtemp
+from typing import List, Optional
+
+from git_machete.git_operations import GitContext
+
+
+def popen(command: str) -> str:
+    with os.popen(command) as process:
+        return process.read().strip()
+
+
+git: GitContext = GitContext()
+
+
+class BaseTest:
+    def setup_method(self) -> None:
+        self.repo_sandbox = GitRepositorySandbox()
+        (
+            self.repo_sandbox
+            # Create the remote and sandbox repos, chdir into sandbox repo
+            .new_repo(self.repo_sandbox.remote_path, "--bare")
+            .new_repo(self.repo_sandbox.local_path)
+            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
+            .execute('git config user.email "tester@test.com"')
+            .execute('git config user.name "Tester Test"')
+        )
+
+
+class GitRepositorySandbox:
+    second_remote_path = mkdtemp()
+
+    def __init__(self) -> None:
+        self.remote_path = mkdtemp()
+        self.local_path = mkdtemp()
+        self.file_counter = 0
+
+    def execute(self, command: str) -> "GitRepositorySandbox":
+        subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
+        return self
+
+    def new_repo(self, *args: str, switch_dir_to_new_repo: bool = True) -> "GitRepositorySandbox":
+        previous_dir = os.getcwd()
+        os.chdir(args[0])
+        opts = args[1:]
+        self.execute(f"git init {' '.join(opts)}")
+        if not switch_dir_to_new_repo:
+            os.chdir(previous_dir)
+        return self
+
+    def new_branch(self, branch_name: str) -> "GitRepositorySandbox":
+        self.execute(f"git checkout -b {branch_name}")
+        return self
+
+    def new_root_branch(self, branch_name: str) -> "GitRepositorySandbox":
+        self.execute(f"git checkout --orphan {branch_name}")
+        return self
+
+    def check_out(self, branch: str) -> "GitRepositorySandbox":
+        self.execute(f"git checkout {branch}")
+        return self
+
+    def commit(self, message: str = "Some commit message.") -> "GitRepositorySandbox":
+        f = f'{self.file_counter}.txt'
+        self.file_counter += 1
+        self.execute(f"touch {f}")
+        self.execute(f"git add {f}")
+        self.execute(f'git commit -m "{message}"')
+        return self
+
+    def add_file_and_commit(self, file_path: str = 'file_name.txt', file_content: str = 'Some file content\n',
+                            message: str = "Some commit message.") -> "GitRepositorySandbox":
+        self.write_to_file(file_path=file_path, file_content=file_content)
+        self.execute(f"git add {file_path}")
+        self.execute(f'git commit -m "{message}"')
+        return self
+
+    def commit_amend(self, message: str) -> "GitRepositorySandbox":
+        self.execute(f'git commit --amend -m "{message}"')
+        return self
+
+    def push(self, remote: str = 'origin', set_upstream: bool = True, tracking_branch: Optional[str] = None) -> "GitRepositorySandbox":
+        branch = popen("git symbolic-ref -q --short HEAD")
+        tracking_branch = tracking_branch or branch
+        self.execute(f"git push {'--set-upstream' if set_upstream else ''} {remote} {branch}:{tracking_branch}")
+        return self
+
+    def merge(self, branch_name: str) -> "GitRepositorySandbox":
+        self.execute(f'git merge {branch_name}')
+        return self
+
+    def reset_to(self, revision: str) -> "GitRepositorySandbox":
+        self.execute(f'git reset --keep "{revision}"')
+        return self
+
+    def delete_branch(self, branch: str) -> "GitRepositorySandbox":
+        self.execute(f'git branch -d "{branch}"')
+        return self
+
+    def add_remote(self, remote: str, url: str) -> "GitRepositorySandbox":
+        self.execute(f'git remote add {remote} {url}')
+        return self
+
+    def remove_remote(self, remote: str) -> "GitRepositorySandbox":
+        self.execute(f'git remote remove {remote}')
+        return self
+
+    def get_local_branches(self) -> List[str]:
+        return subprocess.check_output("git for-each-ref refs/heads/ --format='%(refname:short)'",
+                                       stderr=subprocess.STDOUT, shell=True, encoding='utf-8').splitlines()
+
+    def set_git_config_key(self, key: str, value: str) -> "GitRepositorySandbox":
+        self.execute(f'git config {key} {value}')
+        return self
+
+    def read_file(self, file_name: str) -> str:
+        with open(file_name) as f:
+            return f.read()
+
+    def write_to_file(self, file_path: str, file_content: str) -> "GitRepositorySandbox":
+        dirname = os.path.dirname(file_path)
+        if dirname:
+            os.makedirs(dirname, exist_ok=True)
+        with open(file_path, 'w') as f:
+            f.write(file_content)
+        return self
+
+    def set_file_executable(self, file_name: str) -> "GitRepositorySandbox":
+        os.chmod(file_name, 0o700)
+        return self
+
+    def sleep(self, seconds: int) -> "GitRepositorySandbox":
+        time.sleep(seconds)
+        return self
diff --git a/tests/mockers.py b/tests/mockers.py
index 004d68b..b964acf 100644
--- a/tests/mockers.py
+++ b/tests/mockers.py
@@ -1,23 +1,17 @@
+import contextlib
 import io
-import json
 import os
-import random
-import re
-import string
 import subprocess
 import sys
 import textwrap
-import time
-from contextlib import redirect_stderr, redirect_stdout
-from http import HTTPStatus
-from tempfile import mkdtemp
-from typing import Any, Dict, Iterable, List, Optional, Union
-from urllib.error import HTTPError
-from urllib.parse import ParseResult, parse_qs, urlparse
+from contextlib import (_GeneratorContextManager, redirect_stderr,
+                        redirect_stdout)
+from typing import Any, Callable, Iterable, Iterator, Optional
 
 from git_machete import cli, utils
-from git_machete.git_operations import FullCommitHash, GitContext
+from git_machete.git_operations import FullCommitHash
 from git_machete.utils import dim
+from tests.base_test import git, popen
 
 """
 Usage: mockers.py
@@ -37,328 +31,26 @@ Tips on when and why to use mocking functions:
     * used in tests of these git machete commands: `diff`, `log`, `slide-out`
 """
 
-git: GitContext = GitContext()
-
-
-def popen(command: str) -> str:
-    with os.popen(command) as process:
-        return process.read().strip()
 
+@contextlib.contextmanager
+def overridden_environment(**environ: str) -> Iterator[None]:
+    old_environ = dict(os.environ)
+    os.environ.update(environ)
+    try:
+        yield
+    finally:
+        os.environ.clear()
+        os.environ.update(old_environ)
 
-class GitRepositorySandbox:
-    second_remote_path = mkdtemp()
-
-    def __init__(self) -> None:
-        self.remote_path = mkdtemp()
-        self.local_path = mkdtemp()
-
-    def execute(self, command: str) -> "GitRepositorySandbox":
-        subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
-        return self
-
-    def new_repo(self, *args: str, switch_dir_to_new_repo: bool = True) -> "GitRepositorySandbox":
-        previous_dir = os.getcwd()
-        os.chdir(args[0])
-        opts = args[1:]
-        self.execute(f"git init {' '.join(opts)}")
-        if not switch_dir_to_new_repo:
-            os.chdir(previous_dir)
-        return self
-
-    def new_branch(self, branch_name: str) -> "GitRepositorySandbox":
-        self.execute(f"git checkout -b {branch_name}")
-        return self
 
-    def new_root_branch(self, branch_name: str) -> "GitRepositorySandbox":
-        self.execute(f"git checkout --orphan {branch_name}")
-        return self
-
-    def check_out(self, branch: str) -> "GitRepositorySandbox":
-        self.execute(f"git checkout {branch}")
-        return self
-
-    def commit(self, message: str = "Some commit message.") -> "GitRepositorySandbox":
-        f = f'{"".join(random.choice(string.ascii_letters) for _ in range(20))}.txt'
-        self.execute(f"touch {f}")
-        self.execute(f"git add {f}")
-        self.execute(f'git commit -m "{message}"')
-        return self
-
-    def add_file_with_content_and_commit(self, file_name: str = 'file_name.txt', file_content: str = 'Some file content\n',
-                                         message: str = "Some commit message.") -> "GitRepositorySandbox":
-        self.write_to_file(file_name=file_name, file_content=file_content)
-        self.execute(f"git add {file_name}")
-        self.execute(f'git commit -m "{message}"')
-        return self
-
-    def commit_amend(self, message: str) -> "GitRepositorySandbox":
-        self.execute(f'git commit --amend -m "{message}"')
-        return self
-
-    def push(self, remote: str = 'origin', set_upstream: bool = True, tracking_branch: Optional[str] = None) -> "GitRepositorySandbox":
-        branch = popen("git symbolic-ref -q --short HEAD")
-        tracking_branch = tracking_branch or branch
-        self.execute(f"git push {'--set-upstream' if set_upstream else ''} {remote} {branch}:{tracking_branch}")
-        return self
-
-    def sleep(self, seconds: int) -> "GitRepositorySandbox":
-        time.sleep(seconds)
-        return self
-
-    def reset_to(self, revision: str) -> "GitRepositorySandbox":
-        self.execute(f'git reset --keep "{revision}"')
-        return self
-
-    def delete_branch(self, branch: str) -> "GitRepositorySandbox":
-        self.execute(f'git branch -d "{branch}"')
-        return self
-
-    def add_remote(self, remote: str, url: str) -> "GitRepositorySandbox":
-        self.execute(f'git remote add {remote} {url}')
-        return self
-
-    def remove_remote(self, remote: str) -> "GitRepositorySandbox":
-        self.execute(f'git remote remove {remote}')
-        return self
-
-    def add_git_config_key(self, key: str, value: str) -> "GitRepositorySandbox":
-        self.execute(f'git config {key} {value}')
-        return self
-
-    def write_to_file(self, file_name: str, file_content: str) -> "GitRepositorySandbox":
-        with open(file_name, 'w') as f:
-            f.write(file_content)
-        return self
-
-    def merge(self, branch_name: str) -> "GitRepositorySandbox":
-        self.execute(f'git merge {branch_name}')
-        return self
-
-
-class MockGitHubAPIState:
-    def __init__(self, pulls: List[Dict[str, Any]], issues: Optional[List[Dict[str, Any]]] = None) -> None:
-        self.pulls: List[Dict[str, Any]] = pulls
-        self.user: Dict[str, str] = {'login': 'other_user', 'type': 'User', 'company': 'VirtusLab'}
-        # login must be different from the one used in pull requests, otherwise pull request author will not be annotated
-        self.issues: List[Dict[str, Any]] = issues or []
-
-    def new_request(self) -> "MockGitHubAPIRequest":
-        return MockGitHubAPIRequest(self)
-
-    def get_issue(self, issue_no: str) -> Optional[Dict[str, Any]]:
-        for issue in self.issues:
-            if issue['number'] == issue_no:
-                return issue
-        return None
-
-    def get_pull(self, pull_no: str) -> Optional[Dict[str, Any]]:
-        for pull in self.pulls:
-            if pull['number'] == pull_no:
-                return pull
-        return None
-
-
-class MockGitHubAPIResponse:
-    def __init__(self, status_code: int, response_data: Union[List[Dict[str, Any]], Dict[str, Any]]) -> None:
-        self.response_data: Union[List[Dict[str, Any]], Dict[str, Any]] = response_data
-        self.status_code: int = status_code
-
-    def read(self) -> bytes:
-        return json.dumps(self.response_data).encode()
-
-    def info(self) -> Dict[str, Any]:
-        return {"link": None}
-
-
-class MockGitHubAPIRequest:
-    def __init__(self, github_api_state: MockGitHubAPIState) -> None:
-        self.github_api_state: MockGitHubAPIState = github_api_state
-
-    def __call__(self, url: str, headers: Dict[str, str] = {}, data: Union[str, bytes, None] = None,
-                 method: str = '') -> "MockGitHubAPIResponse":
-        self.parsed_url: ParseResult = urlparse(url, allow_fragments=True)
-        self.parsed_query: Dict[str, List[str]] = parse_qs(self.parsed_url.query)
-        self.json_data: Union[str, bytes, None] = data
-        self.return_data: Optional[Union[List[Dict[str, Any]], Dict[str, Any]]] = None
-        self.headers: Dict[str, str] = headers
-        return self.handle_method(method)
-
-    def handle_method(self, method: str) -> "MockGitHubAPIResponse":
-        if method == "GET":
-            return self.handle_get()
-        elif method == "PATCH":
-            return self.handle_patch()
-        elif method == "POST":
-            return self.handle_post()
-        else:
-            return self.make_response_object(HTTPStatus.METHOD_NOT_ALLOWED, [])
-
-    def handle_get(self) -> "MockGitHubAPIResponse":
-        if 'pulls' in self.parsed_url.path:
-            full_head_name: Optional[List[str]] = self.parsed_query.get('head')
-            number: Optional[str] = self.find_number(self.parsed_url.path, 'pulls')
-            if number:
-                for pr in self.github_api_state.pulls:
-                    if pr['number'] == number:
-                        return self.make_response_object(HTTPStatus.OK, pr)
-                return self.make_response_object(HTTPStatus.NOT_FOUND, [])
-            elif full_head_name:
-                head: str = full_head_name[0].split(':')[1]
-                for pr in self.github_api_state.pulls:
-                    if pr['head']['ref'] == head:
-                        return self.make_response_object(HTTPStatus.OK, [pr])
-                return self.make_response_object(HTTPStatus.NOT_FOUND, [])
-            else:
-                return self.make_response_object(HTTPStatus.OK, [pull for pull in self.github_api_state.pulls if pull['state'] == 'open'])
-        elif self.parsed_url.path.endswith('user'):
-            return self.make_response_object(HTTPStatus.OK, self.github_api_state.user)
-        else:
-            return self.make_response_object(HTTPStatus.NOT_FOUND, [])
-
-    def handle_patch(self) -> "MockGitHubAPIResponse":
-        if 'issues' in self.parsed_url.path:
-            return self.update_issue()
-        elif 'pulls' in self.parsed_url.path:
-            return self.update_pull_request()
-        else:
-            return self.make_response_object(HTTPStatus.NOT_FOUND, [])
-
-    def handle_post(self) -> "MockGitHubAPIResponse":
-        assert not self.parsed_query
-        if 'issues' in self.parsed_url.path:
-            return self.update_issue()
-        elif 'pulls' in self.parsed_url.path:
-            return self.update_pull_request()
-        else:
-            return self.make_response_object(HTTPStatus.NOT_FOUND, [])
-
-    def update_pull_request(self) -> "MockGitHubAPIResponse":
-        pull_no = self.find_number(self.parsed_url.path, 'pulls')
-        if not pull_no:
-            if self.is_pull_created():
-                head = json.loads(self.json_data)["head"]  # type: ignore[arg-type]
-                return self.make_response_object(HTTPStatus.UNPROCESSABLE_ENTITY, {'message': 'Validation Failed', 'errors': [
-                    {'message': f'A pull request already exists for test_repo:{head}.'}]})
-            return self.create_pull_request()
-        pull = self.github_api_state.get_pull(pull_no)
-        assert pull is not None
-        return self.fill_pull_request_data(json.loads(self.json_data), pull)  # type: ignore[arg-type]
-
-    def create_pull_request(self) -> "MockGitHubAPIResponse":
-        pull = {'number': self.get_next_free_number(self.github_api_state.pulls),
-                'user': {'login': 'github_user'},
-                'html_url': 'www.github.com',
-                'state': 'open',
-                'head': {'ref': "", 'repo': {'full_name': 'testing:checkout_prs', 'html_url': mkdtemp()}},
-                'base': {'ref': ""}}
-        return self.fill_pull_request_data(json.loads(self.json_data), pull)  # type: ignore[arg-type]
-
-    def fill_pull_request_data(self, data: Dict[str, Any], pull: Dict[str, Any]) -> "MockGitHubAPIResponse":
-        index = self.get_index_or_none(pull, self.github_api_state.issues)
-        for key in data.keys():
-            if key in ('base', 'head'):
-                pull[key]['ref'] = json.loads(self.json_data)[key]  # type: ignore[arg-type]
-            else:
-                pull[key] = json.loads(self.json_data)[key]  # type: ignore[arg-type]
-        if index:
-            self.github_api_state.pulls[index] = pull
-        else:
-            self.github_api_state.pulls.append(pull)
-        return self.make_response_object(HTTPStatus.CREATED, pull)
-
-    def update_issue(self) -> "MockGitHubAPIResponse":
-        issue_no = self.find_number(self.parsed_url.path, 'issues')
-        if not issue_no:
-            return self.create_issue()
-        issue = self.github_api_state.get_issue(issue_no)
-        assert issue is not None
-        return self.fill_issue_data(json.loads(self.json_data), issue)  # type: ignore[arg-type]
-
-    def create_issue(self) -> "MockGitHubAPIResponse":
-        issue = {'number': self.get_next_free_number(self.github_api_state.issues)}
-        return self.fill_issue_data(json.loads(self.json_data), issue)  # type: ignore[arg-type]
-
-    def fill_issue_data(self, data: Dict[str, Any], issue: Dict[str, Any]) -> "MockGitHubAPIResponse":
-        index = self.get_index_or_none(issue, self.github_api_state.issues)
-        for key in data.keys():
-            issue[key] = data[key]
-        if index is not None:
-            self.github_api_state.issues[index] = issue
-        else:
-            self.github_api_state.issues.append(issue)
-        return self.make_response_object(HTTPStatus.CREATED, issue)
-
-    def is_pull_created(self) -> bool:
-        deserialized_json_data = json.loads(self.json_data)  # type: ignore[arg-type]
-        head: str = deserialized_json_data['head']
-        base: str = deserialized_json_data['base']
-        for pull in self.github_api_state.pulls:
-            pull_head: str = pull['head']['ref']
-            pull_base: str = pull['base']['ref']
-            if (head, base) == (pull_head, pull_base):
-                return True
-        return False
-
-    @staticmethod
-    def get_index_or_none(entity: Dict[str, Any], base: List[Dict[str, Any]]) -> Optional[int]:
-        try:
-            return base.index(entity)
-        except ValueError:
-            return None
-
-    @staticmethod
-    def make_response_object(status_code: int, response_data: Union[List[Dict[str, Any]], Dict[str, Any]]) -> "MockGitHubAPIResponse":
-        return MockGitHubAPIResponse(status_code, response_data)
-
-    @staticmethod
-    def find_number(url: str, entity: str) -> Optional[str]:
-        m = re.search(f'{entity}/(\\d+)', url)
-        if m:
-            return m.group(1)
-        return None
-
-    @staticmethod
-    def get_next_free_number(entities: List[Dict[str, Any]]) -> str:
-        numbers = [int(item['number']) for item in entities]
-        return str(max(numbers) + 1)
-
-
-class MockHTTPError(HTTPError):
-    from email.message import Message
-
-    def __init__(self, url: str, code: int, msg: Any, hdrs: Message, fp: Any) -> None:
-        super().__init__(url, code, msg, hdrs, fp)
-        self.msg = msg
-
-    def read(self, n: int = 1) -> bytes:
-        return json.dumps(self.msg).encode()
-
-
-class MockContextManager:
-    def __init__(self, obj: MockGitHubAPIResponse) -> None:
-        self.obj = obj
-
-    def __enter__(self) -> MockGitHubAPIResponse:
-        if self.obj.status_code == HTTPStatus.NOT_FOUND:
-            raise HTTPError("http://example.org", 404, 'Not found', None, None)  # type: ignore[arg-type]
-        elif self.obj.status_code == HTTPStatus.UNPROCESSABLE_ENTITY:
-            raise MockHTTPError("http://example.org", 422, self.obj.response_data, None, None)  # type: ignore[arg-type]
-        return self.obj
-
-    def __exit__(self, *args: Any) -> None:
-        pass
-
-
-class MockContextManagerRaise403(MockContextManager):
-    def __init__(self, obj: MockGitHubAPIResponse) -> None:
-        super().__init__(obj)
-
-    def __enter__(self) -> MockGitHubAPIResponse:
-        raise HTTPError("http://example.org", 403, 'Forbidden', None, None)  # type: ignore[arg-type]
-
-
-def adapt(s: str, indent: str) -> str:
-    return textwrap.indent(textwrap.dedent(s[1:]), indent)
+def fixed_author_and_committer_date() -> _GeneratorContextManager:  # type: ignore[type-arg]
+    # It doesn't matter WHAT this fixed timestamp is, as long as it's fixed
+    # (and hence, commit hashes are fixed).
+    fixed_committer_and_author_date = 'Mon 20 Aug 2018 20:19:19 +0200'
+    return overridden_environment(
+        GIT_COMMITTER_DATE=fixed_committer_and_author_date,
+        GIT_AUTHOR_DATE=fixed_committer_and_author_date
+    )
 
 
 def launch_command(*args: str) -> str:
@@ -372,10 +64,13 @@ def launch_command(*args: str) -> str:
         return out.getvalue()
 
 
-def assert_command(cmds: Iterable[str], expected_result: str, strip_indentation: bool = True, indent: str = '  ') -> None:
-    expected_result = adapt(expected_result, indent) if strip_indentation else expected_result
-    actual_result = launch_command(*cmds)
-    assert actual_result == expected_result, f'Actual result:\n`{actual_result}`\nExpected result:\n`{expected_result}`'
+def assert_command(cmds: Iterable[str], expected_result: str) -> None:
+    if expected_result.startswith("\n"):
+        # removeprefix is only available since Python 3.9
+        expected_result = expected_result[1:]
+    expected_result = textwrap.dedent(expected_result)
+    actual_result = textwrap.dedent(launch_command(*cmds))
+    assert actual_result == expected_result
 
 
 def rewrite_definition_file(new_body: str, dedent: bool = True) -> None:
@@ -430,6 +125,10 @@ def mock_ask_if(*args: str, **kwargs: Any) -> str:
     return 'y'
 
 
+def mock_ask_if_returning(answer: str) -> Callable[..., str]:
+    return lambda *args, **kwargs: answer
+
+
 def mock_should_perform_interactive_slide_out(cmd: str) -> bool:
     return True
 
diff --git a/tests/test_add.py b/tests/test_add.py
index 16bcaaf..4a6f0b3 100644
--- a/tests/test_add.py
+++ b/tests/test_add.py
@@ -1,27 +1,14 @@
 from typing import Any
 
-from .mockers import (GitRepositorySandbox, assert_command, mock_run_cmd,
-                      rewrite_definition_file)
+from .base_test import BaseTest
+from .mockers import assert_command, mock_run_cmd, rewrite_definition_file
 
 
-class TestAdd:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestAdd(BaseTest):
 
     def test_add(self, mocker: Any) -> None:
         """
-        Verify behaviour of a 'git machete add' command.
+        Verify the behaviour of a 'git machete add' command.
         """
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
 
@@ -49,8 +36,7 @@ class TestAdd:
         assert_command(
             ['add', '-y'],
             'Adding bugfix/feature_fail onto the inferred upstream (parent) branch develop\n'
-            'Added branch bugfix/feature_fail onto develop\n',
-            strip_indentation=False
+            'Added branch bugfix/feature_fail onto develop\n'
         )
 
         self.repo_sandbox.check_out('develop')
@@ -58,8 +44,7 @@ class TestAdd:
         assert_command(
             ['add', '-y', 'bugfix/some_feature'],
             'Adding bugfix/some_feature onto the inferred upstream (parent) branch develop\n'
-            'Added branch bugfix/some_feature onto develop\n',
-            strip_indentation=False
+            'Added branch bugfix/some_feature onto develop\n'
         )
 
         self.repo_sandbox.check_out('develop')
@@ -67,8 +52,7 @@ class TestAdd:
         assert_command(
             ['add', '-y', 'refs/heads/bugfix/another_feature'],
             'Adding bugfix/another_feature onto the inferred upstream (parent) branch develop\n'
-            'Added branch bugfix/another_feature onto develop\n',
-            strip_indentation=False
+            'Added branch bugfix/another_feature onto develop\n'
         )
 
         # test with --onto option
@@ -76,6 +60,33 @@ class TestAdd:
 
         assert_command(
             ['add', '--onto=feature'],
-            'Added branch chore/remove_indentation onto feature\n',
-            strip_indentation=False
+            'Added branch chore/remove_indentation onto feature\n'
+        )
+
+    def test_add_check_out_remote_branch(self, mocker: Any) -> None:
+        """
+        Verify the behaviour of a 'git machete add' command in the special case when a remote branch is checked out locally.
+        """
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+
+        (
+            self.repo_sandbox.new_branch("master")
+            .commit("master commit.")
+            .new_branch("feature/foo")
+            .push()
+            .check_out("master")
+            .delete_branch("feature/foo")
+        )
+
+        assert_command(
+            ['add', '-y', 'foo'],
+            'A local branch foo does not exist. Creating out of the current HEAD\n'
+            'Added branch foo as a new root\n'
+        )
+
+        assert_command(
+            ['add', '-y', '--as-root', 'feature/foo'],
+            'A local branch feature/foo does not exist, but a remote branch origin/feature/foo exists.\n'
+            'Checking out feature/foo locally...\n'
+            'Added branch feature/foo as a new root\n'
         )
diff --git a/tests/test_advance.py b/tests/test_advance.py
index 03cabff..b84e78e 100644
--- a/tests/test_advance.py
+++ b/tests/test_advance.py
@@ -2,25 +2,12 @@ from typing import Any
 
 import pytest
 
-from .mockers import (GitRepositorySandbox, get_commit_hash,
-                      get_current_commit_hash, launch_command, mock_run_cmd,
-                      rewrite_definition_file)
+from .base_test import BaseTest
+from .mockers import (get_commit_hash, get_current_commit_hash, launch_command,
+                      mock_run_cmd, rewrite_definition_file)
 
 
-class TestAdvance:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestAdvance(BaseTest):
 
     def test_advance_for_no_downstream_branches(self, mocker: Any) -> None:
         """Verify behaviour of a 'git machete advance' command.
diff --git a/tests/test_anno.py b/tests/test_anno.py
index cb64914..2f9f59a 100644
--- a/tests/test_anno.py
+++ b/tests/test_anno.py
@@ -1,21 +1,8 @@
-from .mockers import (GitRepositorySandbox, assert_command, launch_command,
-                      rewrite_definition_file)
+from .base_test import BaseTest
+from .mockers import assert_command, launch_command, rewrite_definition_file
 
 
-class TestAnno:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestAnno(BaseTest):
 
     def test_anno(self) -> None:
         """
@@ -100,3 +87,23 @@ class TestAnno:
             x-feature  Custom annotation for `feature` branch (untracked)
             """,
         )
+
+        assert_command(
+            ['anno'],
+            'Custom annotation for `develop` branch\n'
+        )
+
+        assert_command(
+            ['anno', '-b', 'feature'],
+            'Custom annotation for `feature` branch\n'
+        )
+
+        assert_command(
+            ['anno', '-b', 'feature', ''],
+            ""
+        )
+
+        assert_command(
+            ['anno', '-b', 'feature'],
+            ""
+        )
diff --git a/tests/test_clean.py b/tests/test_clean.py
index 6742105..f21dd0a 100644
--- a/tests/test_clean.py
+++ b/tests/test_clean.py
@@ -3,26 +3,13 @@ from typing import Any
 
 import pytest
 
-from .mockers import (GitRepositorySandbox, assert_command, launch_command,
-                      mock_ask_if, mock_run_cmd,
-                      mock_should_perform_interactive_slide_out,
+from .base_test import BaseTest
+from .mockers import (assert_command, launch_command, mock_ask_if,
+                      mock_run_cmd, mock_should_perform_interactive_slide_out,
                       rewrite_definition_file)
 
 
-class TestClean:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestClean(BaseTest):
 
     def test_clean(self, mocker: Any) -> None:
         mocker.patch(
diff --git a/tests/test_client.py b/tests/test_client.py
index ad6cd55..705b112 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -2,26 +2,13 @@ from typing import Dict
 
 from git_machete.annotation import Annotation
 from git_machete.client import MacheteClient
-from git_machete.git_operations import GitContext, LocalBranchShortName
+from git_machete.git_operations import LocalBranchShortName
 
-from .mockers import GitRepositorySandbox, rewrite_definition_file
+from .base_test import BaseTest, git
+from .mockers import rewrite_definition_file
 
 
-class TestClient:
-
-    def setup_method(self) -> None:
-
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestClient(BaseTest):
 
     def test_annotations_read_definition_file(self) -> None:
         """
@@ -70,7 +57,6 @@ class TestClient:
             """
         self.repo_sandbox.new_branch("root")
         rewrite_definition_file(body)
-        git = GitContext()
         machete_client = MacheteClient(git)
         machete_client.read_definition_file(perform_interactive_slide_out=False)
         annotations: Dict[LocalBranchShortName, Annotation] = machete_client.annotations
diff --git a/tests/test_delete_unmanaged.py b/tests/test_delete_unmanaged.py
new file mode 100644
index 0000000..dfe87f4
--- /dev/null
+++ b/tests/test_delete_unmanaged.py
@@ -0,0 +1,62 @@
+from typing import Any
+
+from .base_test import BaseTest
+from .mockers import (assert_command, fixed_author_and_committer_date,
+                      launch_command, mock_ask_if_returning,
+                      mock_run_cmd_and_forward_stdout, rewrite_definition_file)
+
+
+class TestDeleteUnmanaged(BaseTest):
+
+    def test_delete_unmanaged(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_forward_stdout)
+
+        with fixed_author_and_committer_date():
+            (
+                self.repo_sandbox.new_branch("master")
+                    .commit("master commit.")
+                    .new_branch("develop")
+                    .commit("develop commit.")
+                    .new_branch("feature")
+                    .commit("feature commit.")
+            )
+        body: str = \
+            """
+            master
+            """
+        rewrite_definition_file(body)
+
+        assert_command(
+            ["delete-unmanaged", "--yes"],
+            """
+            Checking for unmanaged branches...
+            Skipping current branch feature
+            Deleting branch develop (merged to HEAD)...
+            Deleted branch develop (was 03e727b).
+
+            """
+        )
+
+        self.repo_sandbox.check_out("master")
+        assert_command(
+            ["delete-unmanaged", "-y"],
+            """
+            Checking for unmanaged branches...
+            Deleting branch feature (unmerged to HEAD)...
+            Deleted branch feature (was 87e00e9).
+
+            """
+        )
+
+        assert_command(
+            ["delete-unmanaged", "-y"],
+            """
+            Checking for unmanaged branches...
+            No branches to delete
+            """
+        )
+
+        self.repo_sandbox.new_branch("foo").check_out("master")
+        mocker.patch("git_machete.client.MacheteClient.ask_if", mock_ask_if_returning("n"))
+        launch_command("delete-unmanaged")
+        assert self.repo_sandbox.get_local_branches() == ["foo", "master"]
diff --git a/tests/test_diff.py b/tests/test_diff.py
index dc3345d..d53516a 100644
--- a/tests/test_diff.py
+++ b/tests/test_diff.py
@@ -1,23 +1,10 @@
 from typing import Any
 
-from .mockers import (GitRepositorySandbox, assert_command,
-                      mock_run_cmd_and_forward_stdout)
+from .base_test import BaseTest
+from .mockers import assert_command, mock_run_cmd_and_forward_stdout
 
 
-class TestDiff:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestDiff(BaseTest):
 
     def test_diff(self, mocker: Any) -> None:
         """
@@ -26,14 +13,14 @@ class TestDiff:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_forward_stdout)  # to hide git outputs in tests
         (
             self.repo_sandbox.new_branch("master")
-                .add_file_with_content_and_commit(message='master commit1')
+                .add_file_and_commit(message='master commit1')
                 .push()
                 .new_branch("develop")
-                .add_file_with_content_and_commit(file_name='develop_file_name.txt',
-                                                  file_content='Develop content\n',
-                                                  message='develop commit')
+                .add_file_and_commit(file_path='develop_file_name.txt',
+                                     file_content='Develop content\n',
+                                     message='develop commit')
                 .push()
-                .write_to_file(file_name='file_name.txt',
+                .write_to_file(file_path='file_name.txt',
                                file_content='Content not committed\n')
         )
 
@@ -56,7 +43,7 @@ class TestDiff:
         """
 
         # Test `git machete diff` without providing the branch name, git diff against the current working tree
-        assert_command(["diff"], expected_status_output, indent='')
+        assert_command(["diff"], expected_status_output)
 
         expected_status_output = """
         diff --git a/develop_file_name.txt b/develop_file_name.txt
@@ -68,7 +55,11 @@ class TestDiff:
         +Develop content
 
         """
+        assert_command(["diff", "develop"], expected_status_output)
+        assert_command(["diff", "refs/heads/develop"], expected_status_output)
 
-        assert_command(["diff", "develop"], expected_status_output, indent='')
-
-        assert_command(["diff", "refs/heads/develop"], expected_status_output, indent='')
+        assert_command(
+            ["diff", "--stat", "refs/heads/develop"],
+            "develop_file_name.txt | 1 +\n"
+            "1 file changed, 1 insertion(+)\n\n"
+        )
diff --git a/tests/test_discover.py b/tests/test_discover.py
index dc30c23..62afefb 100644
--- a/tests/test_discover.py
+++ b/tests/test_discover.py
@@ -1,21 +1,8 @@
-from .mockers import (GitRepositorySandbox, assert_command, launch_command,
-                      rewrite_definition_file)
+from .base_test import BaseTest
+from .mockers import assert_command, launch_command, rewrite_definition_file
 
 
-class TestDiscover:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestDiscover(BaseTest):
 
     def test_discover(self) -> None:
         (
@@ -58,3 +45,18 @@ class TestDiscover:
             """
         )
         assert_command(['status'], expected_status_output)
+
+        launch_command('discover', '--roots=feature1,master', '-y')
+        expected_status_output = (
+            """
+            feature1 (untracked)
+            |
+            o-feature2 (untracked)
+
+            master
+            |
+            o-feature3  rebase=no push=no
+              |
+              o-feature4 * (untracked)
+            """
+        )
diff --git a/tests/test_edit.py b/tests/test_edit.py
new file mode 100644
index 0000000..97ad882
--- /dev/null
+++ b/tests/test_edit.py
@@ -0,0 +1,10 @@
+from .base_test import BaseTest
+from .mockers import launch_command, overridden_environment
+
+
+class TestEdit(BaseTest):
+
+    def test_edit_git_machete_editor(self) -> None:
+        with overridden_environment(GIT_MACHETE_EDITOR="bash -c 'echo foo > $1' 'ignored_$0'"):
+            launch_command("edit", "--debug")
+        assert self.repo_sandbox.read_file(".git/machete") == "foo\n"
diff --git a/tests/test_file.py b/tests/test_file.py
index c98e925..a5f6041 100644
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -1,23 +1,11 @@
 import os
 from pathlib import Path
 
-from .mockers import GitContext, GitRepositorySandbox, launch_command
+from .base_test import BaseTest, git
+from .mockers import launch_command
 
 
-class TestFile:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestFile(BaseTest):
 
     def test_file(self) -> None:
         """
@@ -39,7 +27,7 @@ class TestFile:
         definition_file_path_relative_to_git_dir = '/'.join(definition_file_path[-2:]).rstrip('\n')
         assert definition_file_path_relative_to_git_dir == '.git/machete'
 
-        if GitContext().get_git_version() >= (2, 5):  # `git worktree` command was introduced in git version 2.5
+        if git.get_git_version() >= (2, 5):  # `git worktree` command was introduced in git version 2.5
             # check git machete definition file path when inside a worktree using the default `True` value
             # for the `machete.worktree.useTopLevelMacheteFile` key
             self.repo_sandbox.execute("git worktree add -f -b snickers_feature snickers_worktree develop")
@@ -51,7 +39,7 @@ class TestFile:
 
             # check git machete definition file path when inside a worktree
             # but with the `machete.worktree.useTopLevelMacheteFile` key set to `False`
-            self.repo_sandbox.add_git_config_key('machete.worktree.useTopLevelMacheteFile', 'false')
+            self.repo_sandbox.set_git_config_key('machete.worktree.useTopLevelMacheteFile', 'false')
             self.repo_sandbox.execute("git worktree add -f -b mars_feature mars_worktree develop")
             os.chdir('mars_worktree')
             definition_file_full_path = launch_command("file")
diff --git a/tests/test_fork_point.py b/tests/test_fork_point.py
index 4480462..ba33cb1 100644
--- a/tests/test_fork_point.py
+++ b/tests/test_fork_point.py
@@ -1,46 +1,24 @@
-import os
-
-from .mockers import (GitRepositorySandbox, assert_command,
+from .base_test import BaseTest
+from .mockers import (assert_command, fixed_author_and_committer_date,
                       get_current_commit_hash, launch_command,
                       rewrite_definition_file)
 
 
-class TestForkPoint:
-
-    def setup_method(self) -> None:
-
-        self.repo_sandbox = GitRepositorySandbox()
+class TestForkPoint(BaseTest):
 
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
-
-    def test_fork_point(self) -> None:
+    def test_fork_point_get(self) -> None:
         """
         Verify behaviour of a 'git machete fork-point' command.
         """
-        fixed_committer_and_author_date = 'Mon 20 Aug 2018 20:19:19 +0200'
-        os.environ['GIT_COMMITTER_DATE'] = fixed_committer_and_author_date
-        os.environ['GIT_AUTHOR_DATE'] = fixed_committer_and_author_date
-        try:
+        with fixed_author_and_committer_date():
             (
                 self.repo_sandbox.new_branch("master")
-                    .add_file_with_content_and_commit(message="master commit.")
+                    .commit(message="master commit.")
                     .new_branch("develop")
-                    .add_file_with_content_and_commit(file_content='develop content', message="develop commit.")
+                    .commit(message="develop commit.")
                     .new_branch("feature")
                     .commit('feature commit.')
             )
-        finally:
-            # Clean-up environment variables
-            os.environ.pop('GIT_COMMITTER_DATE', None)
-            os.environ.pop('GIT_AUTHOR_DATE', None)
 
         body: str = \
             """
@@ -52,14 +30,15 @@ class TestForkPoint:
 
         # Test `git machete fork-point` without providing the branch name
         # hash 67007ed30def3b9b658380b895a9f62b525286e0 corresponds to the commit on develop branch
-        assert_command(["fork-point"], "67007ed30def3b9b658380b895a9f62b525286e0\n", strip_indentation=False)
+        assert_command(["fork-point"], "03e727bb987b21acce75e404f57e9d33ca876c20\n")
+        assert_command(["fork-point", "--inferred"], "03e727bb987b21acce75e404f57e9d33ca876c20\n")
 
         # hash 515319fa0ab47f372f6159bcc8ac27b43ee8a0ed corresponds to the commit on master branch
-        assert_command(["fork-point", 'develop'], "515319fa0ab47f372f6159bcc8ac27b43ee8a0ed\n", strip_indentation=False)
+        assert_command(["fork-point", 'develop'], "58a3121d3ef89189eb51176c7ec5344f4aab2f84\n")
 
-        assert_command(["fork-point", 'refs/heads/develop'], "515319fa0ab47f372f6159bcc8ac27b43ee8a0ed\n", strip_indentation=False)
+        assert_command(["fork-point", 'refs/heads/develop'], "58a3121d3ef89189eb51176c7ec5344f4aab2f84\n")
 
-    def test_fork_point_override(self) -> None:
+    def test_fork_point_override_to_commit(self) -> None:
         """
         Verify behaviour of a 'git machete fork-point' command with fork-point being overridden by config key.
         """
@@ -70,10 +49,7 @@ class TestForkPoint:
         master_branch_first_commit_hash = get_current_commit_hash()
         self.repo_sandbox.commit("master second commit")
         develop_branch_fork_point = get_current_commit_hash()
-        (
-            self.repo_sandbox.new_branch("develop")
-                .commit("develop commit")
-        )
+        self.repo_sandbox.new_branch("develop").commit("develop commit")
         body: str = \
             """
             master
@@ -82,19 +58,19 @@ class TestForkPoint:
         rewrite_definition_file(body)
 
         # invalid fork point with length not equal to 40
-        self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.to', 39 * 'a')
+        self.repo_sandbox.set_git_config_key('machete.overrideForkPoint.develop.to', 39 * 'a')
         assert launch_command('fork-point').strip() == develop_branch_fork_point
 
         # invalid, non-hexadecimal alphanumeric characters present in the fork point
-        self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.to', 20 * 'g1')
+        self.repo_sandbox.set_git_config_key('machete.overrideForkPoint.develop.to', 20 * 'g1')
         assert launch_command('fork-point').strip() == develop_branch_fork_point
 
         # invalid, non-hexadecimal special characters present in the fork point
-        self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.to', 40 * '#')
+        self.repo_sandbox.set_git_config_key('machete.overrideForkPoint.develop.to', 40 * '#')
         assert launch_command('fork-point').strip() == develop_branch_fork_point
 
         # valid commit hash but not present in the repository
-        self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.to', 40 * 'a')
+        self.repo_sandbox.set_git_config_key('machete.overrideForkPoint.develop.to', 40 * 'a')
         assert launch_command('fork-point').strip() == (
                "Warn: since branch develop is no longer a descendant of commit aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, the fork point override to this commit no longer applies.\n"  # noqa: E501
                "Consider running:\n"
@@ -104,3 +80,73 @@ class TestForkPoint:
         # valid fork-point override commit hash
         launch_command('fork-point', f'--override-to={master_branch_first_commit_hash}')
         assert launch_command('fork-point').strip() == master_branch_first_commit_hash
+        assert launch_command('fork-point', '--inferred').strip() == develop_branch_fork_point
+
+        launch_command('fork-point', '--unset-override')
+        assert launch_command('fork-point').strip() == develop_branch_fork_point
+        assert launch_command('fork-point', '--inferred').strip() == develop_branch_fork_point
+
+    def test_fork_point_override_to_parent_and_inferred(self) -> None:
+        with fixed_author_and_committer_date():
+            (
+                self.repo_sandbox
+                .new_branch("master")
+                .commit("master  commit")
+                .new_branch("in-between")
+                .commit("in-between commit")
+                .new_branch("develop")
+                .commit("develop commit")
+            )
+
+        body: str = \
+            """
+            master
+                develop
+            """
+        rewrite_definition_file(body)
+
+        assert launch_command("fork-point").strip() == "a71ffac2c1d41b8d1592a25f0056e4dfca829608"
+        assert launch_command("fork-point", "--inferred").strip() == "a71ffac2c1d41b8d1592a25f0056e4dfca829608"
+        assert_command(
+            ["status", "--list-commits-with-hashes"],
+            """
+              master (untracked)
+              |
+              | a71ffac  in-between commit -> fork point ??? this commit seems to be a part of the unique history of in-between
+              | 4aed40c  develop commit
+              ?-develop * (untracked)
+
+            Warn: yellow edge indicates that fork point for develop is probably incorrectly inferred,
+            or that some extra branch should be between master and develop.
+
+            Consider using git machete fork-point --override-to=<revision>|--override-to-inferred|--override-to-parent develop,
+            or reattaching develop under a different parent branch.
+            """
+        )
+
+        launch_command("fork-point", "--override-to-parent")
+        assert launch_command("fork-point").strip() == "7e6757a9e7888e8cad7e112ae4dc305966335594"
+        assert launch_command("fork-point", "--inferred").strip() == "a71ffac2c1d41b8d1592a25f0056e4dfca829608"
+        assert_command(
+            ["status", "-L"],
+            """
+              master (untracked)
+              |
+              | a71ffac  in-between commit
+              | 4aed40c  develop commit
+              o-develop * (untracked)
+            """
+        )
+
+        launch_command("fork-point", "--override-to-inferred")
+        assert launch_command("fork-point").strip() == "a71ffac2c1d41b8d1592a25f0056e4dfca829608"
+        assert launch_command("fork-point", "--inferred").strip() == "a71ffac2c1d41b8d1592a25f0056e4dfca829608"
+        assert_command(
+            ["status", "-L"],
+            """
+              master (untracked)
+              |
+              | 4aed40c  develop commit
+              o-develop * (untracked)
+            """
+        )
diff --git a/tests/test_git_operations.py b/tests/test_git_operations.py
index 0855bcf..8c0c3bd 100644
--- a/tests/test_git_operations.py
+++ b/tests/test_git_operations.py
@@ -1,27 +1,13 @@
 from typing import Any
 
-from git_machete.git_operations import (FullCommitHash, GitContext,
+from git_machete.git_operations import (AnyRevision, FullCommitHash,
                                         LocalBranchShortName)
 
-from .mockers import (GitRepositorySandbox, get_current_commit_hash,
-                      mock_run_cmd)
+from .base_test import BaseTest, git
+from .mockers import get_current_commit_hash, mock_run_cmd
 
 
-class TestGitOperations:
-
-    def setup_method(self) -> None:
-
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestGitOperations(BaseTest):
 
     def test_run_git(self, mocker: Any) -> None:
         """
@@ -34,7 +20,6 @@ class TestGitOperations:
         )
         master_branch_first_commit_hash = get_current_commit_hash()
 
-        git = GitContext()
         assert git._run_git("rev-parse", "--verify", "--quiet", master_branch_first_commit_hash + "^{commit}", allow_non_zero=True) == 0  # noqa: FS003, E501
         assert git._run_git("rev-parse", "HEAD") == 0
 
@@ -53,9 +38,12 @@ class TestGitOperations:
                 .new_branch("feature")
                 .commit("feature commit")
         )
-        git = GitContext()
-        assert git.is_commit_present_in_repository(revision=FullCommitHash(40 * 'a')) is False
-        assert git.is_commit_present_in_repository(revision=master_branch_first_commit_hash) is True
+
+        def is_commit_present_in_repository(revision: AnyRevision) -> bool:
+            return git._popen_git("rev-parse", "--verify", "--quiet", revision + "^{commit}", allow_non_zero=True).exit_code == 0  # noqa: FS003, E501
+
+        assert is_commit_present_in_repository(revision=FullCommitHash(40 * 'a')) is False
+        assert is_commit_present_in_repository(revision=master_branch_first_commit_hash) is True
 
         assert git.is_ancestor_or_equal(earlier_revision=LocalBranchShortName('feature'),
                                         later_revision=LocalBranchShortName('master')) is False
diff --git a/tests/test_github.py b/tests/test_github.py
index abab85a..052b99e 100644
--- a/tests/test_github.py
+++ b/tests/test_github.py
@@ -1,10 +1,14 @@
 import json
 import os
+import re
+from http import HTTPStatus
 from subprocess import CalledProcessError, CompletedProcess
 from tempfile import mkdtemp
 from textwrap import dedent
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable, ContextManager, Dict, List, Optional, Union
 from unittest.mock import mock_open
+from urllib.error import HTTPError
+from urllib.parse import ParseResult, parse_qs, urlparse
 
 import pytest
 
@@ -14,11 +18,10 @@ from git_machete.github import (GitHubClient, GitHubToken,
                                 RemoteAndOrganizationAndRepository)
 from git_machete.options import CommandLineOptions
 
-from .mockers import (GitRepositorySandbox, MockContextManager,
-                      MockContextManagerRaise403, MockGitHubAPIState,
-                      MockHTTPError, assert_command, get_current_commit_hash,
-                      git, launch_command, mock_ask_if, mock_exit_script,
-                      mock_run_cmd, mock_should_perform_interactive_slide_out,
+from .base_test import BaseTest, GitRepositorySandbox, git
+from .mockers import (assert_command, get_current_commit_hash, launch_command,
+                      mock_ask_if, mock_exit_script, mock_run_cmd,
+                      mock_should_perform_interactive_slide_out,
                       rewrite_definition_file)
 
 
@@ -128,23 +131,230 @@ def mock_info(x: Any) -> Dict[str, Any]:
 mock_info.counter = mock_read.counter = 0  # type: ignore[attr-defined]
 
 
-class TestGitHub:
-    mock_repository_info: Dict[str, str] = {'full_name': 'testing/checkout_prs',
-                                            'html_url': 'https://github.com/tester/repo_sandbox.git'}
+class MockGitHubAPIState:
+    def __init__(self, pulls: List[Dict[str, Any]], issues: Optional[List[Dict[str, Any]]] = None) -> None:
+        self.pulls: List[Dict[str, Any]] = pulls
+        self.user: Dict[str, str] = {'login': 'other_user', 'type': 'User', 'company': 'VirtusLab'}
+        # login must be different from the one used in pull requests, otherwise pull request author will not be annotated
+        self.issues: List[Dict[str, Any]] = issues or []
 
-    def setup_method(self) -> None:
+    def new_request(self) -> "MockGitHubAPIRequest":
+        return MockGitHubAPIRequest(self)
 
-        self.repo_sandbox = GitRepositorySandbox()
+    def get_issue(self, issue_no: str) -> Optional[Dict[str, Any]]:
+        for issue in self.issues:
+            if issue['number'] == issue_no:
+                return issue
+        return None
 
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+    def get_pull(self, pull_no: str) -> Optional[Dict[str, Any]]:
+        for pull in self.pulls:
+            if pull['number'] == pull_no:
+                return pull
+        return None
+
+
+class MockGitHubAPIResponse:
+    def __init__(self, status_code: int, response_data: Union[List[Dict[str, Any]], Dict[str, Any]]) -> None:
+        self.response_data: Union[List[Dict[str, Any]], Dict[str, Any]] = response_data
+        self.status_code: int = status_code
+
+    def read(self) -> bytes:
+        return json.dumps(self.response_data).encode()
+
+    def info(self) -> Dict[str, Any]:
+        return {"link": None}
+
+
+class MockGitHubAPIRequest:
+    def __init__(self, github_api_state: MockGitHubAPIState) -> None:
+        self.github_api_state: MockGitHubAPIState = github_api_state
+
+    def __call__(self, url: str, headers: Dict[str, str] = {}, data: Union[str, bytes, None] = None,
+                 method: str = '') -> "MockGitHubAPIResponse":
+        self.parsed_url: ParseResult = urlparse(url, allow_fragments=True)
+        self.parsed_query: Dict[str, List[str]] = parse_qs(self.parsed_url.query)
+        self.json_data: Union[str, bytes, None] = data
+        self.headers: Dict[str, str] = headers
+        return self.handle_method(method)
+
+    def handle_method(self, method: str) -> "MockGitHubAPIResponse":
+        if method == "GET":
+            return self.handle_get()
+        elif method == "PATCH":
+            return self.handle_patch()
+        elif method == "POST":
+            return self.handle_post()
+        else:
+            return self.make_response_object(HTTPStatus.METHOD_NOT_ALLOWED, [])
+
+    def handle_get(self) -> "MockGitHubAPIResponse":
+        if 'pulls' in self.parsed_url.path:
+            full_head_name: Optional[List[str]] = self.parsed_query.get('head')
+            number: Optional[str] = self.find_number(self.parsed_url.path, 'pulls')
+            if number:
+                for pr in self.github_api_state.pulls:
+                    if pr['number'] == number:
+                        return self.make_response_object(HTTPStatus.OK, pr)
+                return self.make_response_object(HTTPStatus.NOT_FOUND, [])
+            elif full_head_name:
+                head: str = full_head_name[0].split(':')[1]
+                for pr in self.github_api_state.pulls:
+                    if pr['head']['ref'] == head:
+                        return self.make_response_object(HTTPStatus.OK, [pr])
+                return self.make_response_object(HTTPStatus.NOT_FOUND, [])
+            else:
+                return self.make_response_object(HTTPStatus.OK, [pull for pull in self.github_api_state.pulls if pull['state'] == 'open'])
+        elif self.parsed_url.path.endswith('user'):
+            return self.make_response_object(HTTPStatus.OK, self.github_api_state.user)
+        else:
+            return self.make_response_object(HTTPStatus.NOT_FOUND, [])
+
+    def handle_patch(self) -> "MockGitHubAPIResponse":
+        if 'issues' in self.parsed_url.path:
+            return self.update_issue()
+        elif 'pulls' in self.parsed_url.path:
+            return self.update_pull_request()
+        else:
+            return self.make_response_object(HTTPStatus.NOT_FOUND, [])
+
+    def handle_post(self) -> "MockGitHubAPIResponse":
+        assert not self.parsed_query
+        if 'issues' in self.parsed_url.path:
+            return self.update_issue()
+        elif 'pulls' in self.parsed_url.path:
+            return self.update_pull_request()
+        else:
+            return self.make_response_object(HTTPStatus.NOT_FOUND, [])
+
+    def update_pull_request(self) -> "MockGitHubAPIResponse":
+        pull_no = self.find_number(self.parsed_url.path, 'pulls')
+        if not pull_no:
+            if self.is_pull_created():
+                head = json.loads(self.json_data)["head"]  # type: ignore[arg-type]
+                return self.make_response_object(HTTPStatus.UNPROCESSABLE_ENTITY, {'message': 'Validation Failed', 'errors': [
+                    {'message': f'A pull request already exists for test_repo:{head}.'}]})
+            return self.create_pull_request()
+        pull = self.github_api_state.get_pull(pull_no)
+        assert pull is not None
+        return self.fill_pull_request_data(json.loads(self.json_data), pull)  # type: ignore[arg-type]
+
+    def create_pull_request(self) -> "MockGitHubAPIResponse":
+        pull = {'number': self.get_next_free_number(self.github_api_state.pulls),
+                'user': {'login': 'github_user'},
+                'html_url': 'www.github.com',
+                'state': 'open',
+                'head': {'ref': "", 'repo': {'full_name': 'testing:checkout_prs', 'html_url': mkdtemp()}},
+                'base': {'ref': ""}}
+        return self.fill_pull_request_data(json.loads(self.json_data), pull)  # type: ignore[arg-type]
+
+    def fill_pull_request_data(self, data: Dict[str, Any], pull: Dict[str, Any]) -> "MockGitHubAPIResponse":
+        index = self.get_index_or_none(pull, self.github_api_state.issues)
+        for key in data.keys():
+            if key in ('base', 'head'):
+                pull[key]['ref'] = json.loads(self.json_data)[key]  # type: ignore[arg-type]
+            else:
+                pull[key] = json.loads(self.json_data)[key]  # type: ignore[arg-type]
+        if index:
+            self.github_api_state.pulls[index] = pull
+        else:
+            self.github_api_state.pulls.append(pull)
+        return self.make_response_object(HTTPStatus.CREATED, pull)
+
+    def update_issue(self) -> "MockGitHubAPIResponse":
+        issue_no = self.find_number(self.parsed_url.path, 'issues')
+        if not issue_no:
+            return self.create_issue()
+        issue = self.github_api_state.get_issue(issue_no)
+        assert issue is not None
+        return self.fill_issue_data(json.loads(self.json_data), issue)  # type: ignore[arg-type]
+
+    def create_issue(self) -> "MockGitHubAPIResponse":
+        issue = {'number': self.get_next_free_number(self.github_api_state.issues)}
+        return self.fill_issue_data(json.loads(self.json_data), issue)  # type: ignore[arg-type]
+
+    def fill_issue_data(self, data: Dict[str, Any], issue: Dict[str, Any]) -> "MockGitHubAPIResponse":
+        index = self.get_index_or_none(issue, self.github_api_state.issues)
+        for key in data.keys():
+            issue[key] = data[key]
+        if index is not None:
+            self.github_api_state.issues[index] = issue
+        else:
+            self.github_api_state.issues.append(issue)
+        return self.make_response_object(HTTPStatus.CREATED, issue)
+
+    def is_pull_created(self) -> bool:
+        deserialized_json_data = json.loads(self.json_data)  # type: ignore[arg-type]
+        head: str = deserialized_json_data['head']
+        base: str = deserialized_json_data['base']
+        for pull in self.github_api_state.pulls:
+            pull_head: str = pull['head']['ref']
+            pull_base: str = pull['base']['ref']
+            if (head, base) == (pull_head, pull_base):
+                return True
+        return False
+
+    @staticmethod
+    def get_index_or_none(entity: Dict[str, Any], base: List[Dict[str, Any]]) -> Optional[int]:
+        try:
+            return base.index(entity)
+        except ValueError:
+            return None
+
+    @staticmethod
+    def make_response_object(status_code: int, response_data: Union[List[Dict[str, Any]], Dict[str, Any]]) -> "MockGitHubAPIResponse":
+        return MockGitHubAPIResponse(status_code, response_data)
+
+    @staticmethod
+    def find_number(url: str, entity: str) -> Optional[str]:
+        m = re.search(f'{entity}/(\\d+)', url)
+        if m:
+            return m.group(1)
+        return None
+
+    @staticmethod
+    def get_next_free_number(entities: List[Dict[str, Any]]) -> str:
+        numbers = [int(item['number']) for item in entities]
+        return str(max(numbers) + 1)
+
+
+class MockHTTPError(HTTPError):
+    from email.message import Message
+
+    def __init__(self, url: str, code: int, msg: Any, hdrs: Message, fp: Any) -> None:
+        super().__init__(url, code, msg, hdrs, fp)
+        self.msg = msg
+
+    def read(self, n: int = 1) -> bytes:  # noqa: F841
+        return json.dumps(self.msg).encode()
+
+
+class MockContextManager(ContextManager[MockGitHubAPIResponse]):
+    def __init__(self, obj: MockGitHubAPIResponse) -> None:
+        self.obj = obj
+
+    def __enter__(self) -> MockGitHubAPIResponse:
+        if self.obj.status_code == HTTPStatus.NOT_FOUND:
+            raise HTTPError("http://example.org", 404, 'Not found', None, None)  # type: ignore[arg-type]
+        elif self.obj.status_code == HTTPStatus.UNPROCESSABLE_ENTITY:
+            raise MockHTTPError("http://example.org", 422, self.obj.response_data, None, None)  # type: ignore[arg-type]
+        return self.obj
+
+    def __exit__(self, *args: Any) -> None:
+        pass
+
+
+class MockContextManagerRaise403(MockContextManager):
+    def __init__(self, obj: MockGitHubAPIResponse) -> None:
+        super().__init__(obj)
+
+    def __enter__(self) -> MockGitHubAPIResponse:
+        raise HTTPError("http://example.org", 403, 'Forbidden', None, None)  # type: ignore[arg-type]
+
+
+class TestGitHub(BaseTest):
+    mock_repository_info: Dict[str, str] = {'full_name': 'testing/checkout_prs',
+                                            'html_url': 'https://github.com/tester/repo_sandbox.git'}
 
     git_api_state_for_test_retarget_pr = MockGitHubAPIState(
         [
@@ -219,8 +429,7 @@ class TestGitHub:
 
         assert_command(
             ['github', 'retarget-pr'],
-            'The base branch of PR #15 has been switched to branch-1\n',
-            strip_indentation=False
+            'The base branch of PR #15 has been switched to branch-1\n'
         )
 
         expected_status_output = """
@@ -237,8 +446,7 @@ class TestGitHub:
 
         assert_command(
             ['github', 'retarget-pr'],
-            'The base branch of PR #15 is already branch-1\n',
-            strip_indentation=False
+            'The base branch of PR #15 is already branch-1\n'
         )
 
     git_api_state_for_test_github_retarget_pr_explicit_branch = MockGitHubAPIState(
@@ -304,8 +512,7 @@ class TestGitHub:
 
         assert_command(
             ['github', 'retarget-pr', '--branch', 'feature'],
-            'The base branch of PR #15 has been switched to branch-1\n',
-            strip_indentation=False
+            'The base branch of PR #15 has been switched to branch-1\n'
         )
 
         expected_status_output = """
@@ -332,9 +539,8 @@ class TestGitHub:
                                   ' Visit https://github.com/settings/tokens to generate a new one.')
         with pytest.raises(MacheteException) as e:
             launch_command("github", "retarget-pr", "--branch", "branch-without-pr")
-        if e:
-            assert e.value.args[0] == expected_error_message, \
-                'Verify that expected error message has appeared when there is no pull request associated with that branch name.'
+        assert e.value.args[0] == expected_error_message, \
+            'Verify that expected error message has appeared when there is no pull request associated with that branch name.'
 
         launch_command('github', 'retarget-pr', '--branch', 'branch-without-pr', '--ignore-if-missing')
 
@@ -388,9 +594,8 @@ class TestGitHub:
         )
         with pytest.raises(MacheteException) as e:
             launch_command("github", "retarget-pr")
-        if e:
-            assert e.value.args[0] == expected_error_message, \
-                'Verify that expected error message has appeared when given pull request to create is already created.'
+        assert e.value.args[0] == expected_error_message, \
+            'Verify that expected error message has appeared when given pull request to create is already created.'
 
         # branch feature_1 present in each remote, tracking data present
         (
@@ -412,8 +617,7 @@ class TestGitHub:
 
         assert_command(
             ['github', 'retarget-pr'],
-            'The base branch of PR #20 has been switched to feature\n',
-            strip_indentation=False
+            'The base branch of PR #20 has been switched to feature\n'
         )
 
         # branch feature_2 is not present in any of the remotes
@@ -435,9 +639,8 @@ class TestGitHub:
 
         with pytest.raises(MacheteException) as e:
             launch_command("github", "retarget-pr")
-        if e:
-            assert e.value.args[0] == expected_error_message, \
-                'Verify that expected error message has appeared when given pull request to create is already created.'
+        assert e.value.args[0] == expected_error_message, \
+            'Verify that expected error message has appeared when given pull request to create is already created.'
 
         # branch feature_2 present in only one remote: origin_1 and there is no tracking data available -> infer the remote
         (
@@ -447,8 +650,7 @@ class TestGitHub:
 
         assert_command(
             ['github', 'retarget-pr'],
-            'The base branch of PR #25 has been switched to feature\n',
-            strip_indentation=False
+            'The base branch of PR #25 has been switched to feature\n'
         )
 
         # branch feature_3 present in only one remote: origin_1 and has tracking data
@@ -472,8 +674,7 @@ class TestGitHub:
 
         assert_command(
             ['github', 'retarget-pr'],
-            'The base branch of PR #35 has been switched to feature_2\n',
-            strip_indentation=False
+            'The base branch of PR #35 has been switched to feature_2\n'
         )
 
     git_api_state_for_test_anno_prs = MockGitHubAPIState(
@@ -594,9 +795,9 @@ class TestGitHub:
         (
             self.repo_sandbox
                 .remove_remote('new_origin')
-                .add_git_config_key('machete.github.remote', 'custom_origin')
-                .add_git_config_key('machete.github.organization', 'custom_user')
-                .add_git_config_key('machete.github.repository', 'custom_repo')
+                .set_git_config_key('machete.github.remote', 'custom_origin')
+                .set_git_config_key('machete.github.organization', 'custom_user')
+                .set_git_config_key('machete.github.repository', 'custom_repo')
         )
 
         launch_command('github', 'anno-prs')
@@ -791,9 +992,8 @@ class TestGitHub:
         expected_error_message = "A pull request already exists for test_repo:hotfix/add-trigger."
         with pytest.raises(MacheteException) as e:
             launch_command("github", "create-pr")
-        if e:
-            assert e.value.args[0] == expected_error_message, \
-                'Verify that expected error message has appeared when given pull request to create is already created.'
+        assert e.value.args[0] == expected_error_message, \
+            'Verify that expected error message has appeared when given pull request to create is already created.'
 
         # check against head branch is ancestor or equal to base branch
         (
@@ -820,18 +1020,16 @@ class TestGitHub:
                                  "Cannot create pull request."
         with pytest.raises(MacheteException) as e:
             launch_command("github", "create-pr")
-        if e:
-            assert e.value.parameter == expected_error_message, \
-                'Verify that expected error message has appeared when head branch is equal or ancestor of base branch.'
+        assert e.value.parameter == expected_error_message, \
+            'Verify that expected error message has appeared when head branch is equal or ancestor of base branch.'
 
         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 pytest.raises(MacheteException) as e:
             launch_command("github", "create-pr")
-        if e:
-            assert e.value.parameter == expected_error_message, \
-                'Verify that expected error message has appeared when creating PR from root branch.'
+        assert e.value.parameter == expected_error_message, \
+            'Verify that expected error message has appeared when creating PR from root branch.'
 
     git_api_state_for_test_create_pr_missing_base_branch_on_remote = MockGitHubAPIState(
         [
@@ -881,7 +1079,7 @@ class TestGitHub:
         expected_msg = ("Fetching origin...\n"
                         "Warn: Base branch for this PR (feature/api_handling) is not found on remote, pushing...\n"
                         "Creating a PR from feature/api_exception_handling to feature/api_handling... OK, see www.github.com\n")
-        assert_command(['github', 'create-pr'], expected_msg, strip_indentation=False)
+        assert_command(['github', 'create-pr'], expected_msg)
         assert_command(
             ['status'],
             """
@@ -974,8 +1172,7 @@ class TestGitHub:
         """  # noqa: W291, E501
         assert_command(
             ['github', 'create-pr'],
-            expected_result,
-            indent=''
+            expected_result
         )
         # branch feature_1 present in each of the remotes, tracking data present
         (
@@ -993,8 +1190,7 @@ class TestGitHub:
         """
         assert_command(
             ['github', 'create-pr'],
-            expected_result,
-            indent=''
+            expected_result
         )
 
         # branch feature_2 not present in any of the remotes, remote origin_1 picked manually via mock_input()
@@ -1026,8 +1222,7 @@ class TestGitHub:
         """  # noqa: W291
         assert_command(
             ['github', 'create-pr'],
-            expected_result,
-            indent=''
+            expected_result
         )
 
         # branch feature_2 present in only one remote: origin_1, no tracking data
@@ -1045,8 +1240,7 @@ class TestGitHub:
         """  # noqa: E501
         assert_command(
             ['github', 'create-pr'],
-            expected_result,
-            indent=''
+            expected_result
         )
 
         # branch feature_3 present in only one remote: origin_2, tracking data present
@@ -1065,8 +1259,7 @@ class TestGitHub:
         """
         assert_command(
             ['github', 'create-pr'],
-            expected_result,
-            indent=''
+            expected_result
         )
 
         # branch feature_3 present in only one remote: origin_2 with tracking data, origin remote present - takes priority
@@ -1085,8 +1278,7 @@ class TestGitHub:
         """
         assert_command(
             ['github', 'create-pr'],
-            expected_result,
-            indent=''
+            expected_result
         )
 
     git_api_state_for_test_checkout_prs = MockGitHubAPIState(
@@ -1381,21 +1573,19 @@ class TestGitHub:
         expected_error_message = f"PR #100 is not found in repository {remote_org_repo.organization}/{remote_org_repo.repository}"
         with pytest.raises(MacheteException) as e:
             launch_command('github', 'checkout-prs', '100')
-        if e:
-            assert e.value.parameter == expected_error_message, \
-                'Verify that expected error message has appeared when given pull request to checkout does not exists.'
+        assert e.value.parameter == expected_error_message, \
+            'Verify that expected error message has appeared when given pull request to checkout does not exists.'
 
         with pytest.raises(MacheteException) as e:
             launch_command('github', 'checkout-prs', '19', '100')
-        if e:
-            assert e.value.parameter == expected_error_message, \
-                'Verify that expected error message has appeared when one of the given pull requests to checkout does not exists.'
+        assert e.value.parameter == expected_error_message, \
+            'Verify that expected error message has appeared when one of the given pull requests to checkout does not exists.'
 
         # check against user with no open pull requests
         expected_msg = ("Checking for open GitHub PRs... OK\n"
                         f"Warn: User tester has no open pull request in repository "
                         f"{remote_org_repo.organization}/{remote_org_repo.repository}\n")
-        assert_command(['github', 'checkout-prs', '--by', 'tester'], expected_msg, strip_indentation=False)
+        assert_command(['github', 'checkout-prs', '--by', 'tester'], expected_msg)
 
         # Check against closed pull request with head branch deleted from remote
         local_path = tmp_path
@@ -1414,10 +1604,9 @@ class TestGitHub:
                                  "is already deleted from testing."
         with pytest.raises(MacheteException) as e:
             launch_command('github', 'checkout-prs', '5')
-        if e:
-            assert e.value.parameter == expected_error_message, \
-                'Verify that expected error message has appeared when given pull request to checkout ' \
-                'have already deleted branch from remote.'
+        assert e.value.parameter == expected_error_message, \
+            'Verify that expected error message has appeared when given pull request to checkout ' \
+            'have already deleted branch from remote.'
 
         # Check against pr come from fork
         os.chdir(local_path)
@@ -1432,12 +1621,12 @@ class TestGitHub:
                         "Warn: Pull request #5 is already closed.\n"
                         "Pull request #5 checked out at local branch bugfix/remove-n-option\n")
 
-        assert_command(['github', 'checkout-prs', '5'], expected_msg, strip_indentation=False)
+        assert_command(['github', 'checkout-prs', '5'], expected_msg)
 
         # Check against multiple PRs
         expected_msg = 'Checking for open GitHub PRs... OK\n'
 
-        assert_command(['github', 'checkout-prs', '3', '12'], expected_msg, strip_indentation=False)
+        assert_command(['github', 'checkout-prs', '3', '12'], expected_msg)
 
     git_api_state_for_test_github_checkout_prs_fresh_repo = MockGitHubAPIState(
         [
@@ -1538,8 +1727,7 @@ class TestGitHub:
                         "Pull request #2 checked out at local branch comments/add_docstrings\n")
         assert_command(
             ['github', 'checkout-prs', '2'],
-            expected_msg,
-            strip_indentation=False
+            expected_msg
         )
 
         assert_command(
@@ -1563,8 +1751,7 @@ class TestGitHub:
 
         assert_command(
             ['github', 'checkout-prs', '23'],
-            expected_msg,
-            strip_indentation=False
+            expected_msg
         )
         assert_command(
             ["status"],
@@ -1633,8 +1820,7 @@ class TestGitHub:
                         "Pull request #2 checked out at local branch feature/allow_checkout\n")
         assert_command(
             ['github', 'checkout-prs', '2'],
-            expected_msg,
-            strip_indentation=False
+            expected_msg
         )
 
         assert 'feature/allow_checkout' == launch_command("show", "current").strip(), \
@@ -1991,8 +2177,8 @@ class TestGitHub:
         mocker.patch('urllib.request.urlopen', MockContextManager)
         mocker.patch('git_machete.github.GitHubClient.derive_current_user_login', mock_derive_current_user_login)
         mocker.patch('urllib.request.Request', MockGitHubAPIState([]).new_request())
-        mocker.patch('tests.mockers.MockGitHubAPIResponse.info', mock_info)
-        mocker.patch('tests.mockers.MockGitHubAPIResponse.read', mock_read)
+        mocker.patch('tests.test_github.MockGitHubAPIResponse.info', mock_info)
+        mocker.patch('tests.test_github.MockGitHubAPIResponse.read', mock_read)
 
         global prs_per_page
         (
@@ -2015,8 +2201,8 @@ class TestGitHub:
 
         launch_command('github', 'checkout-prs', '--all')
         launch_command('discover', '--checked-out-since=1 day ago')
-        expected_status_output = '  develop *\n' + '\n'.join([f' |\n o-feature_{i}  rebase=no push=no'
-                                                              for i in range(number_of_pages * prs_per_page)]) + '\n'
+        expected_status_output = 'develop *\n' + '\n'.join([f'|\no-feature_{i}  rebase=no push=no'
+                                                            for i in range(number_of_pages * prs_per_page)]) + '\n'
         assert_command(['status'], expected_status_output)
 
     def test_github_enterprise_domain_fail(self, mocker: Any) -> None:
@@ -2030,7 +2216,7 @@ class TestGitHub:
         mocker.patch('git_machete.cli.exit_script', mock_exit_script)
 
         github_enterprise_domain = 'git.example.org'
-        self.repo_sandbox.add_git_config_key('machete.github.domain', github_enterprise_domain)
+        self.repo_sandbox.set_git_config_key('machete.github.domain', github_enterprise_domain)
 
         expected_error_message = (
             "GitHub API returned `403` HTTP status with error message: `Forbidden`\n"
@@ -2041,8 +2227,7 @@ class TestGitHub:
 
         with pytest.raises(MacheteException) as e:
             launch_command('github', 'checkout-prs', '--all')
-        if e:
-            assert e.value.args[0] == expected_error_message, 'Verify that expected error message has appeared.'
+        assert e.value.args[0] == expected_error_message, 'Verify that expected error message has appeared.'
 
     git_api_state_for_test_github_enterprise_domain = MockGitHubAPIState(
         [
@@ -2078,7 +2263,7 @@ class TestGitHub:
             .push()
             .check_out("develop")
             .delete_branch("snickers")
-            .add_git_config_key('machete.github.domain', github_enterprise_domain)
+            .set_git_config_key('machete.github.domain', github_enterprise_domain)
         )
         launch_command('github', 'checkout-prs', '--all')
 
diff --git a/tests/test_go.py b/tests/test_go.py
index e0c2a82..f492102 100644
--- a/tests/test_go.py
+++ b/tests/test_go.py
@@ -1,23 +1,10 @@
 from typing import Any
 
-from .mockers import (GitRepositorySandbox, launch_command, mock_run_cmd,
-                      rewrite_definition_file)
+from .base_test import BaseTest
+from .mockers import launch_command, mock_run_cmd, rewrite_definition_file
 
 
-class TestGo:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestGo(BaseTest):
 
     def test_go_up(self, mocker: Any) -> None:
         """Verify behaviour of a 'git machete go up' command.
diff --git a/tests/test_help.py b/tests/test_help.py
index 90482da..8956d7f 100644
--- a/tests/test_help.py
+++ b/tests/test_help.py
@@ -5,57 +5,43 @@ import pytest
 from git_machete.cli import commands_and_aliases
 from git_machete.exceptions import ExitCode
 
-from .mockers import (GitRepositorySandbox, launch_command,
-                      mock_exit_script_no_exit, mock_run_cmd)
+from .base_test import BaseTest
+from .mockers import launch_command, mock_exit_script_no_exit, mock_run_cmd
 
 
-class TestHelp:
-
-    def setup_method(self) -> None:
-
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestHelp(BaseTest):
 
     def test_help(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
 
-        help_topics: List[str] = ['config', 'format', 'hooks']
+        with pytest.raises(SystemExit) as e:
+            launch_command()
+        assert ExitCode.ARGUMENT_ERROR == e.value.code
 
         with pytest.raises(SystemExit) as e:
             launch_command("help")
-        assert ExitCode.SUCCESS == e.value.code, \
-            "Verify that `git machete help` causes SystemExit with " \
-            f"{ExitCode.SUCCESS} exit code."
+        assert ExitCode.SUCCESS == e.value.code
+
+        with pytest.raises(SystemExit) as e:
+            launch_command("help", "no-such-command")
+        assert ExitCode.ARGUMENT_ERROR == e.value.code
+
+        help_topics: List[str] = ['config', 'format', 'hooks']
 
         for command in commands_and_aliases:
 
             with pytest.raises(SystemExit) as e:
                 launch_command("help", command)
-            assert ExitCode.SUCCESS == e.value.code, \
-                f"Verify that `git machete help {command}` causes SystemExit" \
-                f" with {ExitCode.SUCCESS} exit code."
+            assert ExitCode.SUCCESS == e.value.code
 
             if command not in help_topics:
                 with pytest.raises(SystemExit) as e:
                     launch_command(command, "--help")
-                assert ExitCode.SUCCESS == e.value.code, \
-                    f"Verify that `git machete {command} --help` causes " \
-                    f"SystemExit with {ExitCode.SUCCESS} exit code."
+                assert ExitCode.SUCCESS == e.value.code
             else:
                 with pytest.raises(SystemExit) as e:
                     launch_command(command, "--help")
-                assert ExitCode.ARGUMENT_ERROR == e.value.code, \
-                    f"Verify that `git machete {command} --help` causes " \
-                    f"SystemExit with {ExitCode.ARGUMENT_ERROR} exit code."
+                assert ExitCode.ARGUMENT_ERROR == e.value.code
 
     def test_help_output_has_no_ansi_codes(self, mocker: Any) -> None:
         mocker.patch('git_machete.cli.exit_script', mock_exit_script_no_exit)
diff --git a/tests/test_is_managed.py b/tests/test_is_managed.py
index 9817c72..9c99236 100644
--- a/tests/test_is_managed.py
+++ b/tests/test_is_managed.py
@@ -1,23 +1,10 @@
 import pytest
 
-from .mockers import (GitRepositorySandbox, launch_command,
-                      rewrite_definition_file)
+from .base_test import BaseTest
+from .mockers import launch_command, rewrite_definition_file
 
 
-class TestIsManaged:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestIsManaged(BaseTest):
 
     def test_is_managed(self) -> None:
         """
diff --git a/tests/test_list.py b/tests/test_list.py
index 71a4df9..821bb76 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -1,23 +1,15 @@
 from typing import Any
 
-from .mockers import (GitRepositorySandbox, assert_command, launch_command,
-                      mock_run_cmd, rewrite_definition_file)
+import pytest
 
+from git_machete.exceptions import ExitCode
 
-class TestList:
+from .base_test import BaseTest
+from .mockers import (assert_command, launch_command, mock_run_cmd,
+                      rewrite_definition_file)
 
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
 
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestList(BaseTest):
 
     def test_list(self, mocker: Any) -> None:
         """
@@ -57,9 +49,14 @@ class TestList:
         rewrite_definition_file(body)
 
         (
-            self.repo_sandbox.check_out("develop")
-                             .new_branch("feature_2")
-                             .commit("feature_2 commit.")
+            self.repo_sandbox
+                .check_out("develop")
+                .new_branch("feature_2")
+                .commit("feature_2 commit.")
+                .new_branch("feature_3")
+                .push()
+                .check_out("feature_2")
+                .delete_branch("feature_3")
         )
 
         expected_output = """
@@ -73,17 +70,16 @@ class TestList:
         """
         assert_command(
             ['list', 'managed'],
-            expected_output,
-            indent=''
+            expected_output
         )
 
         expected_output = """
         feature_2
+        feature_3
         """
         assert_command(
             ['list', 'addable'],
-            expected_output,
-            indent=''
+            expected_output
         )
 
         expected_output = """
@@ -94,8 +90,7 @@ class TestList:
         """
         assert_command(
             ['list', 'childless'],
-            expected_output,
-            indent=''
+            expected_output
         )
 
         expected_output = """
@@ -107,8 +102,7 @@ class TestList:
         """
         assert_command(
             ['list', 'slidable'],
-            expected_output,
-            indent=''
+            expected_output
         )
 
         expected_output = """
@@ -116,8 +110,7 @@ class TestList:
         """
         assert_command(
             ['list', 'slidable-after', 'feature_0_0'],
-            expected_output,
-            indent=''
+            expected_output
         )
 
         expected_output = """
@@ -125,8 +118,7 @@ class TestList:
         """
         assert_command(
             ['list', 'unmanaged'],
-            expected_output,
-            indent=''
+            expected_output
         )
 
         self.repo_sandbox.check_out("feature_1")
@@ -137,6 +129,15 @@ class TestList:
         """
         assert_command(
             ['list', 'with-overridden-fork-point'],
-            expected_output,
-            indent=''
+            expected_output
         )
+
+        launch_command('fork-point', '--unset-override')
+        assert_command(
+            ['list', 'with-overridden-fork-point'],
+            ""
+        )
+
+        with pytest.raises(SystemExit) as e:
+            launch_command('list', 'no-such-category')
+        assert ExitCode.ARGUMENT_ERROR == e.value.code
diff --git a/tests/test_log.py b/tests/test_log.py
index 8381ff4..55b60a1 100644
--- a/tests/test_log.py
+++ b/tests/test_log.py
@@ -1,23 +1,11 @@
 from typing import Any
 
-from .mockers import (GitRepositorySandbox, get_current_commit_hash,
-                      launch_command, mock_run_cmd_and_forward_stdout)
+from .base_test import BaseTest
+from .mockers import (get_current_commit_hash, launch_command,
+                      mock_run_cmd_and_forward_stdout)
 
 
-class TestLog:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestLog(BaseTest):
 
     def test_log(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_forward_stdout)
diff --git a/tests/test_reapply.py b/tests/test_reapply.py
new file mode 100644
index 0000000..e1233e1
--- /dev/null
+++ b/tests/test_reapply.py
@@ -0,0 +1,103 @@
+from typing import Any
+
+from .base_test import BaseTest
+from .mockers import (assert_command, fixed_author_and_committer_date,
+                      launch_command, mock_run_cmd, overridden_environment,
+                      rewrite_definition_file)
+
+
+class TestReapply(BaseTest):
+
+    def test_reapply(self, mocker: Any) -> None:
+        """
+        Verify that 'git machete reapply' performs
+        'git rebase' to the fork point of the current branch.
+        """
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+
+        with fixed_author_and_committer_date():
+            (
+                self.repo_sandbox
+                .remove_remote("origin")
+                .new_branch("level-0-branch")
+                .commit("Basic commit.")
+                .new_branch("level-1-branch")
+                .commit("First level-1 commit.")
+                .commit("Second level-1 commit.")
+                .new_branch("level-2-branch")
+                .commit("First level-2 commit.")
+                .commit("Second level-2 commit.")
+                .commit("Third level-2 commit.")
+                .check_out("level-0-branch")
+                .commit("New commit on level-0-branch")
+            )
+
+        body: str = \
+            """
+            level-0-branch
+
+                level-1-branch
+                    level-2-branch
+            """
+        rewrite_definition_file(body)
+
+        self.repo_sandbox.check_out("level-1-branch")
+        assert_command(
+            ["status", "-L"],
+            """
+            level-0-branch
+            |
+            | b98ae42  First level-1 commit.
+            | 1b657a1  Second level-1 commit.
+            x-level-1-branch *
+              |
+              | 64f8913  First level-2 commit.
+              | a6b9ae5  Second level-2 commit.
+              | 958f91f  Third level-2 commit.
+              o-level-2-branch
+            """
+        )
+        assert launch_command("fork-point", "level-1-branch").strip() == "c0306cdd500fc39869505592200258055407bcc6"
+
+        # Let's substitute the editor opened by git for interactive rebase to-do list
+        # so that the test can run in a fully automated manner.
+        with overridden_environment(GIT_SEQUENCE_EDITOR="sed -i.bak '2s/^pick /fixup /'"):
+            with fixed_author_and_committer_date():
+                launch_command("reapply")
+
+        assert_command(
+            ["status", "-L"],
+            """
+            level-0-branch
+            |
+            | 887182d  First level-1 commit.
+            x-level-1-branch *
+              |
+              | 64f8913  First level-2 commit.
+              | a6b9ae5  Second level-2 commit.
+              | 958f91f  Third level-2 commit.
+              x-level-2-branch
+            """
+        )
+        assert launch_command("fork-point", "level-1-branch").strip() == "c0306cdd500fc39869505592200258055407bcc6"
+
+        self.repo_sandbox.check_out("level-2-branch")
+        assert launch_command("fork-point", "level-2-branch").strip() == "1b657a15fa4c619fcb4e871176d1471cdbce9093"
+        with overridden_environment(GIT_SEQUENCE_EDITOR="sed -i.bak '2s/^pick /fixup /'"):
+            with fixed_author_and_committer_date():
+                launch_command("reapply", "--fork-point=64f8913")
+
+        assert_command(
+            ["status", "-L"],
+            """
+            level-0-branch
+            |
+            | 887182d  First level-1 commit.
+            x-level-1-branch
+              |
+              | 64f8913  First level-2 commit.
+              | a5fcbb0  Second level-2 commit.
+              x-level-2-branch *
+            """
+        )
+        assert launch_command("fork-point", "level-2-branch").strip() == "1b657a15fa4c619fcb4e871176d1471cdbce9093"
diff --git a/tests/test_show.py b/tests/test_show.py
index 3271e28..d641272 100644
--- a/tests/test_show.py
+++ b/tests/test_show.py
@@ -1,23 +1,11 @@
 from typing import Any
 
-from .mockers import (GitRepositorySandbox, assert_command, launch_command,
-                      mock_run_cmd, rewrite_definition_file)
+from .base_test import BaseTest
+from .mockers import (assert_command, launch_command, mock_run_cmd,
+                      rewrite_definition_file)
 
 
-class TestShow:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestShow(BaseTest):
 
     def setup_discover_standard_tree(self) -> None:
         (
diff --git a/tests/test_slide_out.py b/tests/test_slide_out.py
index 3a5fb94..75d269a 100644
--- a/tests/test_slide_out.py
+++ b/tests/test_slide_out.py
@@ -2,25 +2,13 @@ from typing import Any
 
 import pytest
 
-from .mockers import (GitRepositorySandbox, assert_command,
-                      get_current_commit_hash, launch_command, mock_run_cmd,
+from .base_test import BaseTest, git
+from .mockers import (assert_command, get_current_commit_hash, launch_command,
+                      mock_ask_if, mock_run_cmd,
                       mock_run_cmd_and_forward_stdout, rewrite_definition_file)
 
 
-class TestSlideOut:
-
-    def setup_method(self, mocker: Any) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestSlideOut(BaseTest):
 
     def test_slide_out(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
@@ -166,10 +154,16 @@ class TestSlideOut:
             """,
         )
 
-        # Slide-out a terminal branch. (child_d)
+        # Slide-out and delete a terminal branch (child_d).
         # This just slices the branch off the tree.
         launch_command("go", "down")
-        launch_command("slide-out", "-n")
+        assert "child_d" in git.get_local_branches()
+        mocker.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
+        assert_command(
+            ["slide-out", "-n", "--delete"],
+            ""
+        )
+        assert "child_d" not in git.get_local_branches()
 
         assert_command(
             ["status", "-l"],
diff --git a/tests/test_squash.py b/tests/test_squash.py
index 43913d0..c470f58 100644
--- a/tests/test_squash.py
+++ b/tests/test_squash.py
@@ -1,23 +1,10 @@
 import pytest
 
-from .mockers import (GitRepositorySandbox, get_current_commit_hash,
-                      launch_command, popen)
+from .base_test import BaseTest, popen
+from .mockers import get_current_commit_hash, launch_command
 
 
-class TestSquash:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestSquash(BaseTest):
 
     def test_squash_with_valid_fork_point(self) -> None:
         (
diff --git a/tests/test_status.py b/tests/test_status.py
index 97d11c6..ea156d9 100644
--- a/tests/test_status.py
+++ b/tests/test_status.py
@@ -6,47 +6,99 @@ import pytest
 
 from git_machete.exceptions import MacheteException
 
-from .mockers import (GitRepositorySandbox, assert_command, launch_command,
-                      mock_exit_script, mock_run_cmd, rewrite_definition_file)
+from .base_test import BaseTest
+from .mockers import (assert_command, launch_command, mock_exit_script,
+                      mock_run_cmd, rewrite_definition_file)
 
 
-class TestStatus:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestStatus(BaseTest):
 
     def test_branch_reappears_in_definition(self, mocker: Any) -> None:
         mocker.patch('git_machete.cli.exit_script', mock_exit_script)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
 
         body: str = \
-            """master
+            """
+            master
             \tdevelop
             \t\n
             develop
             """
+        rewrite_definition_file(body)
+
+        expected_error_message: str = '.git/machete, line 6: branch develop re-appears in the tree definition. ' \
+                                      'Edit the definition file manually with git machete edit'
+
+        with pytest.raises(MacheteException) as e:
+            launch_command('status')
+        assert e.value.parameter == expected_error_message
 
-        self.repo_sandbox.new_branch("root")
+    def test_indent_not_multiply_of_base_indent(self, mocker: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+
+        body: str = \
+            """
+            master
+            \tdevelop
+            \t foo
+            """
         rewrite_definition_file(body)
 
-        expected_error_message: str = '.git/machete, line 5: branch develop re-appears in the tree definition. ' \
+        expected_error_message: str = '.git/machete, line 4: invalid indent <TAB><SPACE>, expected a multiply of <TAB>. ' \
                                       'Edit the definition file manually with git machete edit'
 
         with pytest.raises(MacheteException) as e:
             launch_command('status')
-        if e:
-            assert e.value.parameter == expected_error_message, \
-                'Verify that expected error message has appeared if a branch re-appears in tree definition.'
+        assert e.value.parameter == expected_error_message
+
+    def test_status_branch_hook_output(self) -> None:
+        (
+            self.repo_sandbox
+            .remove_remote('origin')
+            .new_branch('master')
+            .commit('master commit')
+            .new_branch('develop')
+            .commit('develop commit')
+        )
+
+        body: str = \
+            """
+            master
+              develop
+            """
+        rewrite_definition_file(body)
+
+        self.repo_sandbox.write_to_file(".git/hooks/machete-status-branch", "#!/bin/bash\ngit ls-tree $1 | wc -l | sed 's/ *//'")
+        assert_command(
+            ["status"],
+            """
+            hint: The '.git/hooks/machete-status-branch' hook was ignored because it's not set as executable.
+            hint: You can disable this warning with `git config advice.ignoredHook false`.
+              master
+              |
+              o-develop *
+            """
+        )
+
+        self.repo_sandbox.set_git_config_key("advice.ignoredHook", "false")
+        assert_command(
+            ["status"],
+            """
+            master
+            |
+            o-develop *
+            """
+        )
+
+        self.repo_sandbox.set_file_executable(".git/hooks/machete-status-branch")
+        assert_command(
+            ["status"],
+            """
+            master  1
+            |
+            o-develop *  2
+            """
+        )
 
     def test_extra_space_before_branch_name(self, mocker: Any) -> None:
         mocker.patch('git_machete.cli.exit_script', mock_exit_script)
@@ -63,7 +115,7 @@ class TestStatus:
                 .new_branch('foo')
                 .commit()
                 .push()
-                .add_git_config_key('machete.status.extraSpaceBeforeBranchName', 'true')
+                .set_git_config_key('machete.status.extraSpaceBeforeBranchName', 'true')
         )
         body: str = \
             """
@@ -82,9 +134,9 @@ class TestStatus:
                o- foo *
             """
         )
-        assert_command(['status'], expected_status_output, indent='   ')
+        assert_command(['status'], expected_status_output)
 
-        self.repo_sandbox.add_git_config_key('machete.status.extraSpaceBeforeBranchName', 'false')
+        self.repo_sandbox.set_git_config_key('machete.status.extraSpaceBeforeBranchName', 'false')
 
         expected_status_output = (
             """
diff --git a/tests/test_traverse.py b/tests/test_traverse.py
index 0af7e21..814830a 100644
--- a/tests/test_traverse.py
+++ b/tests/test_traverse.py
@@ -1,25 +1,17 @@
 from typing import Any
 
-from .mockers import (GitRepositorySandbox, assert_command, launch_command,
-                      mock_run_cmd, rewrite_definition_file)
+import pytest
 
+from git_machete.exceptions import MacheteException
 
-class TestTraverse:
+from .base_test import BaseTest, git
+from .mockers import (assert_command, launch_command, mock_exit_script,
+                      mock_run_cmd, rewrite_definition_file)
 
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
 
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestTraverse(BaseTest):
 
-    def setup_discover_standard_tree(self) -> None:
+    def setup_standard_tree(self) -> None:
         (
             self.repo_sandbox.new_branch("root")
             .commit("root")
@@ -95,7 +87,7 @@ class TestTraverse:
 
     def test_traverse_no_push(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        self.setup_discover_standard_tree()
+        self.setup_standard_tree()
 
         launch_command("traverse", "-Wy", "--no-push")
         assert_command(
@@ -130,7 +122,7 @@ class TestTraverse:
 
     def test_traverse_no_push_override(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        self.setup_discover_standard_tree()
+        self.setup_standard_tree()
         self.repo_sandbox.check_out("hotfix/add-trigger")
         launch_command("t", "-Wy", "--no-push", "--push", "--start-from=here")
         assert_command(
@@ -163,40 +155,77 @@ class TestTraverse:
             """,
         )
         self.repo_sandbox.check_out("ignore-trailing")
-        launch_command("t", "-Wy", "--no-push", "--push")
+        self.repo_sandbox.set_git_config_key("machete.traverse.push", "false")
+        launch_command("t", "-Wy")
         assert_command(
-            ["status", "-l"],
+            ["status"],
+            """
+            develop
+            |
+            o-allow-ownership-link (diverged from origin)
+            | |
+            | o-build-chain (untracked)
+            |
+            o-call-ws (ahead of origin)
+              |
+              o-drop-constraint (untracked)
+
+            master
+            |
+            o-hotfix/add-trigger
+              |
+              o-ignore-trailing *
+            """,
+        )
+
+        launch_command("t", "-Wy", "--push-untracked")
+        assert_command(
+            ["status"],
+            """
+            develop
+            |
+            o-allow-ownership-link (diverged from origin)
+            | |
+            | o-build-chain
+            |
+            o-call-ws (ahead of origin)
+              |
+              o-drop-constraint
+
+            master
+            |
+            o-hotfix/add-trigger
+              |
+              o-ignore-trailing *
+            """,
+        )
+
+        self.repo_sandbox.set_git_config_key("machete.traverse.push", "true")
+        launch_command("t", "-Wy", "--no-push-untracked")
+        assert_command(
+            ["status"],
             """
             develop
             |
-            | Allow ownership links
-            | 1st round of fixes
             o-allow-ownership-link
             | |
-            | | Build arbitrarily long chains
             | o-build-chain
             |
-            | Call web service
-            | 1st round of fixes
-            | 2nd round of fixes
             o-call-ws
               |
-              | Drop unneeded SQL constraints
               o-drop-constraint
 
             master
             |
-            | HOTFIX Add the trigger (amended)
             o-hotfix/add-trigger
               |
-              | Ignore trailing data (amended)
               o-ignore-trailing *
             """,
         )
 
     def test_traverse_no_push_untracked(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        self.setup_discover_standard_tree()
+        self.setup_standard_tree()
 
         launch_command("traverse", "-Wy", "--no-push-untracked")
         assert_command(
@@ -231,8 +260,8 @@ class TestTraverse:
 
     def test_traverse_push_config_key(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        self.setup_discover_standard_tree()
-        self.repo_sandbox.add_git_config_key('machete.traverse.push', 'false')
+        self.setup_standard_tree()
+        self.repo_sandbox.set_git_config_key('machete.traverse.push', 'false')
         launch_command("traverse", "-Wy")
         assert_command(
             ["status", "-l"],
@@ -336,14 +365,46 @@ class TestTraverse:
         Returned to the initial branch hotfix/add-trigger
         '''
         assert_command(["traverse", "-Wy", "--no-push"],
-                       expected_result,
-                       indent='')
+                       expected_result)
 
-    def test_discover_traverse_squash(self, mocker: Any) -> None:
+    def test_traverse_and_squash(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        self.setup_discover_standard_tree()
+        self.setup_standard_tree()
 
-        launch_command("traverse", "-Wy")
+        self.repo_sandbox.check_out("hotfix/add-trigger")
+        launch_command("traverse", "--fetch", "--start-from=root", "--return-to=here", "-y")
+        assert_command(
+            ["status", "-l"],
+            """
+            develop
+            |
+            | Allow ownership links
+            | 1st round of fixes
+            x-allow-ownership-link (ahead of origin)
+            | |
+            | | Build arbitrarily long chains
+            | x-build-chain (untracked)
+            |
+            | Call web service
+            | 1st round of fixes
+            | 2nd round of fixes
+            o-call-ws (ahead of origin)
+              |
+              | Drop unneeded SQL constraints
+              x-drop-constraint (untracked)
+
+            master
+            |
+            | HOTFIX Add the trigger (amended)
+            o-hotfix/add-trigger *
+              |
+              | Ignore trailing data (amended)
+              o-ignore-trailing
+            """,
+        )
+        assert git.get_current_branch() == "hotfix/add-trigger"
+
+        launch_command("traverse", "-wy")
         assert_command(
             ["status", "-l"],
             """
@@ -367,15 +428,15 @@ class TestTraverse:
             master
             |
             | HOTFIX Add the trigger (amended)
-            o-hotfix/add-trigger
+            o-hotfix/add-trigger *
               |
               | Ignore trailing data (amended)
-              o-ignore-trailing *
+              o-ignore-trailing
             """,
         )
 
-        # Go from ignore-trailing to call-ws which has >1 commit to be squashed
-        for _ in range(4):
+        # Go from hotfix/add-trigger to call-ws which has >1 commit to be squashed
+        for _ in range(3):
             launch_command("go", "prev")
         launch_command("squash")
         assert_command(
@@ -429,7 +490,7 @@ class TestTraverse:
                     snickers
             """
         rewrite_definition_file(body)
-        launch_command("traverse", '-y', '-M', '-n')
+        launch_command("traverse", '-y', '-M', '--no-edit-merge')
         assert_command(
             ["status"],
             """
@@ -443,7 +504,7 @@ class TestTraverse:
 
     def test_traverse_qualifiers_no_push(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        self.setup_discover_standard_tree()
+        self.setup_standard_tree()
 
         body: str = \
             """
@@ -458,7 +519,7 @@ class TestTraverse:
             """
         rewrite_definition_file(body)
 
-        launch_command("traverse", "-Wy")
+        launch_command("traverse", "-Wy", "--no-push-untracked", "--push-untracked")
         assert_command(
             ["status"],
             """
@@ -482,7 +543,7 @@ class TestTraverse:
 
     def test_traverse_qualifiers_no_rebase(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        self.setup_discover_standard_tree()
+        self.setup_standard_tree()
 
         body: str = \
             """
@@ -521,7 +582,7 @@ class TestTraverse:
 
     def test_traverse_qualifiers_no_rebase_no_push(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        self.setup_discover_standard_tree()
+        self.setup_standard_tree()
 
         body: str = \
             """
@@ -560,7 +621,7 @@ class TestTraverse:
 
     def test_traverse_qualifiers_no_slide_out(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        self.setup_discover_standard_tree()
+        self.setup_standard_tree()
 
         body: str = \
             """
@@ -597,3 +658,11 @@ class TestTraverse:
               o-ignore-trailing
             """,
         )
+
+    def test_traverse_no_managed_branches(self, mocker: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script)
+
+        with pytest.raises(MacheteException) as e:
+            launch_command("traverse")
+        assert e.value.args[0] == "No branches listed in .git/machete; " \
+                                  "use `git machete discover` or `git machete edit`, or edit .git/machete manually."
diff --git a/tests/test_update.py b/tests/test_update.py
index 3331bd7..8e66ff3 100644
--- a/tests/test_update.py
+++ b/tests/test_update.py
@@ -1,30 +1,16 @@
-import os
 from typing import Any
 
 import pytest
 
 from git_machete.exceptions import MacheteException
-from git_machete.git_operations import GitContext
 
-from .mockers import (GitRepositorySandbox, get_current_commit_hash,
-                      launch_command, mock_exit_script, mock_run_cmd, popen,
+from .base_test import BaseTest, git, popen
+from .mockers import (get_current_commit_hash, launch_command,
+                      mock_exit_script, mock_run_cmd, overridden_environment,
                       rewrite_definition_file)
 
 
-class TestUpdate:
-
-    def setup_method(self) -> None:
-        self.repo_sandbox = GitRepositorySandbox()
-
-        (
-            self.repo_sandbox
-            # Create the remote and sandbox repos, chdir into sandbox repo
-            .new_repo(self.repo_sandbox.remote_path, "--bare")
-            .new_repo(self.repo_sandbox.local_path)
-            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
-            .execute('git config user.email "tester@test.com"')
-            .execute('git config user.name "Tester Test"')
-        )
+class TestUpdate(BaseTest):
 
     def test_update_with_fork_point_not_specified(self, mocker: Any) -> None:
         """Verify behaviour of a 'git machete update --no-interactive-rebase' command.
@@ -68,8 +54,6 @@ class TestUpdate:
 
         Verify that 'git machete update' drops effectively-empty commits if the underlying git supports that behavior.
         """
-        git = GitContext()
-
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
 
         (
@@ -91,21 +75,20 @@ class TestUpdate:
 
         parents_new_commit_hash = get_current_commit_hash()
         self.repo_sandbox.check_out("level-1-branch")
-        try:
-            os.environ["GIT_SEQUENCE_EDITOR"] = ":"
+        # Note that `--empty=drop` is the default in NON-interactive mode.
+        # We want to check if effectively empty commits are dropped in interactive mode as well.
+        # Let's substitute the editor opened by git for interactive rebase to-do list
+        # so that the test can run in a fully automated manner.
+        with overridden_environment(GIT_SEQUENCE_EDITOR=":"):
             if git.get_git_version() >= (2, 26, 0):
                 launch_command("update")
             else:
                 mocker.patch('git_machete.cli.exit_script', mock_exit_script)
-                try:
+                with pytest.raises(MacheteException) as e:
                     launch_command("update")
-                except MacheteException:
-                    pass
-                else:
-                    raise Exception("`git machete update` was supposed to fail")
+                assert "git rebase --interactive --onto refs/heads/level-0-branch" in e.value.parameter
+                assert "level-1-branch returned 1" in e.value.parameter
                 self.repo_sandbox.execute("git rebase --continue")
-        finally:
-            del os.environ["GIT_SEQUENCE_EDITOR"]
 
         new_fork_point_hash = launch_command("fork-point").strip()
         assert parents_new_commit_hash == new_fork_point_hash, \
diff --git a/tests/test_version.py b/tests/test_version.py
new file mode 100644
index 0000000..65f2b70
--- /dev/null
+++ b/tests/test_version.py
@@ -0,0 +1,17 @@
+from typing import Any
+
+from git_machete import __version__
+
+from .mockers import assert_command, mock_exit_script_no_exit
+
+
+class TestVersion:
+
+    def test_version(self, mocker: Any) -> None:
+        mocker.patch('git_machete.cli.exit_script', mock_exit_script_no_exit)
+        mocker.patch('sys.exit', mock_exit_script_no_exit)
+
+        assert_command(
+            ["version"],
+            f"git-machete version {__version__}\n"
+        )