Repository: VirtusLab/git-machete
Pull request #954 (2023-05-20 07:21:57)

Add missing tests - vol. 6


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index afd290d..51cc411 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,7 +2,8 @@
 
 ## New in git-machete 3.17.5
 
-- fixed: `machete-status-branch` hook can now be executed on Windows
+- fixed: `machete-post-slide-out`, `machete-pre-rebase` and `machete-status-branch` hooks can now be executed on Windows
+- fixed: unstable behavior after `edit` option has been selected for interactively sliding out invalid branches
 
 ## New in git-machete 3.17.4
 
diff --git a/git_machete/cli.py b/git_machete/cli.py
index cdff79e..7abe435 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -727,8 +727,6 @@ def launch(orig_args: List[str]) -> None:
                 fork_point_hash=cli_opts.opt_fork_point, branch=current_branch)
 
         reapply_fork_point = cli_opts.opt_fork_point or machete_client.fork_point(branch=current_branch, use_overrides=True)
-        if not reapply_fork_point:
-            raise MacheteException(f"Fork point not found for branch <b>{current_branch}</b>; use `--fork-point` flag")
 
         git.rebase_onto_ancestor_commit(current_branch, reapply_fork_point, cli_opts.opt_no_interactive_rebase)
     elif cmd == "show":
@@ -770,9 +768,6 @@ def launch(orig_args: List[str]) -> None:
                 fork_point_hash=cli_opts.opt_fork_point, branch=current_branch)
 
         squash_fork_point = cli_opts.opt_fork_point or machete_client.fork_point(branch=current_branch, use_overrides=True)
-        if not squash_fork_point:
-            raise MacheteException(f"Fork point not found for branch <b>{current_branch}</b>; use `--fork-point` flag")
-
         machete_client.squash(current_branch=current_branch, opt_fork_point=squash_fork_point)
     elif cmd in {"status", alias_by_command["status"]}:
         machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
diff --git a/git_machete/client.py b/git_machete/client.py
index 3fd2f8e..0e76b83 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -5,7 +5,7 @@ import os
 import shutil
 import sys
 from collections import OrderedDict
-from typing import Callable, Dict, Iterator, List, Optional, Tuple
+from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
 
 from git_machete import git_config_keys, utils
 from git_machete.annotation import Annotation
@@ -40,6 +40,9 @@ class MacheteClient:
     def __init__(self, git: GitContext) -> None:
         self.__git: GitContext = git
         self._definition_file_path: str = self.__git.get_git_machete_definition_file_path()
+        self.__init_state()
+
+    def __init_state(self) -> None:
         self._managed_branches: List[LocalBranchShortName] = []
         self._up_branch: Dict[LocalBranchShortName, LocalBranchShortName] = {}
         self.__down_branches: Dict[LocalBranchShortName, List[LocalBranchShortName]] = {}
@@ -81,7 +84,7 @@ class MacheteClient:
         if branch not in self.managed_branches:
             raise MacheteException(
                 f"Branch {bold(branch)} not found in the tree of branch dependencies.\n"
-                f"Use `git machete add {branch}` or `git machete edit`")
+                f"Use `git machete add {branch}` or `git machete edit`.")
 
     def expect_at_least_one_managed_branch(self) -> None:
         if not self.__roots:
@@ -103,7 +106,7 @@ class MacheteClient:
 
         invalid_branches: List[LocalBranchShortName] = []
         for index, line in enumerate(lines):
-            if line == "" or line.isspace():
+            if line == "":
                 continue
             prefix = "".join(itertools.takewhile(str.isspace, line))
             if prefix and not self.__indent:
@@ -201,6 +204,7 @@ class MacheteClient:
             self.save_definition_file()
         elif ans in ('e', 'edit'):
             self.edit()
+            self.__init_state()
             self.read_definition_file(verify_branches)
 
     def render_tree(self) -> List[str]:
@@ -260,8 +264,8 @@ class MacheteClient:
             else:
                 out_of = LocalBranchShortName.of(opt_onto).full_name() if opt_onto else HEAD
                 out_of_str = f"{bold(opt_onto)}" if opt_onto else "the current HEAD"
-                msg = (f"A local branch {bold(branch)} does not exist. Create (out "
-                       f"of {out_of_str})?" + get_pretty_choices('y', 'N'))
+                msg = (f"A local branch {bold(branch)} does not exist. Create out "
+                       f"of {out_of_str}?" + get_pretty_choices('y', 'N'))
                 opt_yes_msg = (f"A local branch {bold(branch)} does not exist. "
                                f"Creating out of {out_of_str}")
                 if self.ask_if(msg, opt_yes_msg, opt_yes=opt_yes, verbose=verbose) in ('y', 'yes'):
@@ -347,10 +351,7 @@ class MacheteClient:
                                                 "Rebasing onto the inferred upstream <b>%s</b>..."))
             rebase_fork_point = opt_fork_point or self.fork_point(current_branch, use_overrides=True)
 
-            if not rebase_fork_point:
-                raise MacheteException(f"Fork point not found for branch <b>{current_branch}</b>; use `--fork-point` flag")
-
-            self.__git.rebase(
+            self.rebase(
                 LocalBranchShortName.of(onto_branch).full_name(),
                 rebase_fork_point,
                 current_branch, opt_no_interactive_rebase)
@@ -432,7 +433,7 @@ class MacheteClient:
                 reject_reason_message=("choosing this candidate would form a "
                                        "cycle in the resulting graph or the candidate is a stale branch"))
             if upstream:
-                debug(f"inferred upstream of {branch} is {upstream}, attaching {branch} as a child of {upstream}\n")
+                debug(f"inferred upstream of {branch} is {upstream}, attaching {branch} as a child of {upstream}")
                 self.up_branch[branch] = upstream
                 root_of[branch] = upstream
                 if upstream in self.__down_branches:
@@ -440,7 +441,7 @@ class MacheteClient:
                 else:
                     self.__down_branches[upstream] = [branch]
             else:
-                debug(f"inferred no upstream for {branch}, attaching {branch} as a new root\n")
+                debug(f"inferred no upstream for {branch}, attaching {branch} as a new root")
                 self.__roots += [branch]
 
         # Let's remove merged branches for which no downstream branch have been found.
@@ -454,8 +455,7 @@ class MacheteClient:
                         opt_no_detect_squash_merges=False
                 ):
                     debug(f"inferred upstream of {branch} is {upstream}, but "
-                          f"{branch} is merged to {upstream}; skipping {branch}"
-                          f" from discovered tree\n")
+                          f"{branch} is merged to {upstream}; skipping {branch} from discovered tree")
                     merged_branches_to_skip += [branch]
         if merged_branches_to_skip:
             warn(
@@ -519,14 +519,16 @@ class MacheteClient:
             children_of_the_last_branch_to_slide_out = self.__down_branches.get(last_branch_to_slide_out)
 
             if children_of_the_last_branch_to_slide_out and len(children_of_the_last_branch_to_slide_out) > 1:
-                raise MacheteException(
-                    "Last branch to slide out can't have more than one child branch "
-                    "if option `--down-fork-point` is passed.")
+                raise MacheteException("Last branch to slide out can't have more than one child branch "
+                                       "if option `--down-fork-point` is passed")
 
             if children_of_the_last_branch_to_slide_out:
                 self.check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch(
                     fork_point_hash=opt_down_fork_point,
                     branch=children_of_the_last_branch_to_slide_out[0])
+            else:
+                raise MacheteException("Last branch to slide out must have a child branch "
+                                       "if option `--down-fork-point` is passed")
 
         # Verify that all "interior" slide-out branches have a single downstream pointing to the next slide-out
         for bu, bd in zip(branches_to_slide_out[:-1], branches_to_slide_out[1:]):
@@ -540,9 +542,6 @@ class MacheteClient:
             elif dbs != [bd]:
                 raise MacheteException(f"{bold(bd)} is not downstream of {bold(bu)}, cannot slide out")
 
-            if self.up_branch[bd] != bu:
-                raise MacheteException(f"{bold(bu)} is not upstream of {bold(bd)}, cannot slide out")
-
         # Get new branches
         new_upstream = self.up_branch[branches_to_slide_out[0]]
         new_downstreams = self.__down_branches.get(branches_to_slide_out[-1]) or []
@@ -577,9 +576,7 @@ class MacheteClient:
             else:
                 print(f"Rebasing {bold(new_downstream)} onto {bold(new_upstream)}...")
                 down_fork_point = opt_down_fork_point or self.fork_point(new_downstream, use_overrides=True)
-                if not down_fork_point:
-                    raise MacheteException(f"Fork point not found for branch <b>{new_downstream}</b>; use `--down-fork-point` flag")
-                self.__git.rebase(
+                self.rebase(
                     new_upstream.full_name(),
                     down_fork_point,
                     new_downstream,
@@ -795,8 +792,9 @@ class MacheteClient:
                     self.__run_post_slide_out_hook(upstream, branch, dbb)
                     if ans == 'yq':
                         return
-                    # No need to flush caches since nothing changed in commit/branch
-                    # structure (only machete-specific changes happened).
+                    # Even though the slide-out itself only changes machete branch layout and not git data,
+                    # let's still flush caches due to the changes that might've been performed by post-slide-out hook.
+                    self.flush_caches()
                     continue  # No need to sync branch 'branch' with remote since it just got removed from the tree of dependencies.
                 elif ans in ('q', 'quit'):
                     return
@@ -829,11 +827,8 @@ class MacheteClient:
                             return
                     else:
                         fork_point = self.fork_point(branch, use_overrides=True)
-                        if not fork_point:
-                            raise MacheteException(f"Fork point not found for branch <b>{branch}</b>; "
-                                                   f"use `git machete fork-point {branch} --override-to...`")
 
-                        self.__git.rebase(
+                        self.rebase(
                             LocalBranchShortName.of(upstream).full_name(), fork_point,
                             branch, opt_no_interactive_rebase)
                         # It's clearly possible that rebase can be in progress
@@ -1107,10 +1102,7 @@ class MacheteClient:
             if hook_executable:
                 debug(f"running machete-status-branch hook ({hook_path}) for branch {branch}")
                 hook_env = dict(os.environ, ASCII_ONLY=str(utils.ascii_only).lower())
-                if sys.platform == "win32":
-                    status_code, stdout, stderr = utils.popen_cmd("sh", hook_path, branch, cwd=self.__git.get_root_dir(), env=hook_env)
-                else:
-                    status_code, stdout, stderr = utils.popen_cmd(hook_path, branch, cwd=self.__git.get_root_dir(), env=hook_env)
+                status_code, stdout, stderr = self.__popen_hook(hook_path, branch, cwd=self.__git.get_root_dir(), env=hook_env)
 
                 if status_code == 0:
                     if not stdout.isspace():
@@ -1153,6 +1145,34 @@ class MacheteClient:
             print("", file=sys.stderr)
             warn(f"{first_part}.\n\n{second_part}.")
 
+    @staticmethod
+    def __popen_hook(*args: str, **kwargs: Any) -> Tuple[int, str, str]:
+        if sys.platform == "win32":
+            # This is a poor-man's solution to the problem of Windows **not** recognizing Unix-style shebangs :/
+            return utils.popen_cmd("sh", *args, **kwargs)  # pragma: no cover
+        else:
+            return utils.popen_cmd(*args, **kwargs)
+
+    @staticmethod
+    def __run_hook(*args: str, **kwargs: Any) -> int:
+        if sys.platform == "win32":
+            return utils.run_cmd("sh", *args, **kwargs)  # pragma: no cover
+        else:
+            return utils.run_cmd(*args, **kwargs)
+
+    def rebase(self, onto: AnyRevision, from_exclusive: AnyRevision, branch: LocalBranchShortName, opt_no_interactive_rebase: bool) -> None:
+        hook_path = self.__git.get_hook_path("machete-pre-rebase")
+        if self.__git.check_hook_executable(hook_path):
+            debug(f"running machete-pre-rebase hook ({hook_path})")
+            exit_code = self.__run_hook(hook_path, onto, from_exclusive, branch, cwd=self.__git.get_root_dir())
+            if exit_code == 0:
+                self.__git.rebase(onto, from_exclusive, branch, opt_no_interactive_rebase)
+            else:
+                raise MacheteException(
+                    f"The machete-pre-rebase hook refused to rebase. Error code: {exit_code}")
+        else:
+            self.__git.rebase(onto, from_exclusive, branch, opt_no_interactive_rebase)
+
     def delete_unmanaged(self, *, opt_yes: bool) -> None:
         print('Checking for unmanaged branches...')
         branches_to_delete = excluding(self.__git.get_local_branches(), self.managed_branches)
@@ -1208,27 +1228,31 @@ class MacheteClient:
     def __fork_point_and_containing_branch_pairs(self,
                                                  branch: LocalBranchShortName,
                                                  use_overrides: bool
-                                                 ) -> Tuple[Optional[FullCommitHash], List[BranchPair]]:
+                                                 ) -> Tuple[FullCommitHash, List[BranchPair]]:
         upstream = self.up_branch.get(branch)
+        upstream_hash = self.__git.get_commit_hash_by_revision(upstream) if upstream else None
 
         if use_overrides:
             overridden_fp_hash = self.__get_overridden_fork_point(branch)
             if overridden_fp_hash:
-                if upstream and \
+                if upstream and upstream_hash and \
                         self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()) and \
                         not self.__git.is_ancestor_or_equal(upstream.full_name(), overridden_fp_hash):
                     # We need to handle the case when branch is a descendant of upstream,
-                    # but the fork point of branch is overridden to a commit that
-                    # is NOT a descendant of upstream. In this case it's more
-                    # reasonable to assume that upstream (and not overridden_fp_hash)
-                    # is the fork point.
+                    # but the fork point of branch is overridden to a commit that is NOT a descendant of upstream.
+                    # In this case it's more reasonable to assume that upstream (and not overridden_fp_hash) is the fork point.
                     debug(
                         f"{branch} is descendant of its upstream {upstream}, but overridden fork point commit {overridden_fp_hash} "
                         f"is NOT a descendant of {upstream}; falling back to {upstream} as fork point")
-                    return self.__git.get_commit_hash_by_revision(upstream), []
+                    return upstream_hash, []
                 elif upstream and \
                         self.__git.is_ancestor_or_equal(overridden_fp_hash, upstream.full_name()):
-                    return self.__git.get_merge_base(upstream.full_name(), branch.full_name()), []
+                    common_ancestor = self.__git.get_merge_base(upstream.full_name(), branch.full_name())
+                    if common_ancestor:
+                        return common_ancestor, []
+                    raise MacheteException(f"Fork point not found for branch <b>{branch}</b>; "
+                                           f"use `git machete fork-point {branch} --override-to...`")
+
                 else:
                     debug(f"fork point of {branch} is overridden to {overridden_fp_hash}; skipping inference")
                     return overridden_fp_hash, []
@@ -1236,25 +1260,27 @@ class MacheteClient:
         try:
             fp_hash, containing_branch_pairs = next(self.__match_log_to_filtered_reflogs(branch))
         except StopIteration:
-            if upstream and self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()):
-                debug(
-                    f"cannot find fork point, but {branch} is a descendant of its upstream {upstream}; "
-                    f"falling back to {upstream} as fork point")
-                return self.__git.get_commit_hash_by_revision(upstream), []
-            elif upstream and not self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()):
-                common_ancestor_hash = self.__git.get_merge_base(upstream.full_name(), branch.full_name())
-                if common_ancestor_hash:
+            if upstream and upstream_hash:
+                if self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()):
                     debug(
-                        f"cannot find fork point, and {branch} is NOT a descendant of its upstream {upstream}; "
-                        f"falling back to common ancestor of {branch} and {upstream} (commit {common_ancestor_hash}) as fork point")
-                    return common_ancestor_hash, []
-            raise MacheteException(f"Cannot find fork point for branch {bold(branch)}")
+                        f"cannot find fork point, but {branch} is a descendant of its upstream {upstream}; "
+                        f"falling back to {upstream} as fork point")
+                    return upstream_hash, []
+                else:
+                    common_ancestor_hash = self.__git.get_merge_base(upstream.full_name(), branch.full_name())
+                    if common_ancestor_hash:
+                        debug(
+                            f"cannot find fork point, and {branch} is NOT a descendant of its upstream {upstream}; "
+                            f"falling back to common ancestor of {branch} and {upstream} (commit {common_ancestor_hash}) as fork point")
+                        return common_ancestor_hash, []
+            raise MacheteException(f"Fork point not found for branch <b>{branch}</b>; "
+                                   f"use `git machete fork-point {branch} --override-to...`")
         else:
             debug(f"commit {fp_hash} is the most recent point in history of {branch} to occur on "
                   "filtered reflog of any other branch or its remote counterpart "
                   f"(specifically: {' and '.join(map(utils.get_second, containing_branch_pairs))})")
 
-            if upstream and \
+            if upstream and upstream_hash and \
                     self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()) and \
                     not self.__git.is_ancestor_or_equal(upstream.full_name(), fp_hash):
                 # That happens very rarely in practice (typically current head
@@ -1266,7 +1292,7 @@ class MacheteClient:
                     f"{upstream} is an ancestor of its upstream {branch}, "
                     f"but the inferred fork point commit {fp_hash} is NOT a descendant of {upstream}; "
                     f"falling back to {upstream} as fork point")
-                return self.__git.get_commit_hash_by_revision(upstream), []
+                return upstream_hash, []
             elif upstream and \
                     not self.__git.is_ancestor_or_equal(upstream.full_name(), branch.full_name()) and \
                     self.__git.is_ancestor_or_equal(fp_hash, upstream.full_name()):
@@ -1287,16 +1313,13 @@ class MacheteClient:
             self,
             branch: LocalBranchShortName,
             use_overrides: bool
-    ) -> Optional[FullCommitHash]:
+    ) -> FullCommitHash:
         hash, containing_branch_pairs = self.__fork_point_and_containing_branch_pairs(branch, use_overrides)
-        return FullCommitHash.of(hash) if hash else None
+        return FullCommitHash.of(hash)
 
     def diff(self, *, branch: Optional[LocalBranchShortName], opt_stat: bool) -> None:
         diff_branch = branch or self.__git.get_current_branch()
         fork_point = self.fork_point(diff_branch, use_overrides=True)
-        if not fork_point:
-            raise MacheteException(f"Fork point not found for branch <b>{diff_branch}</b>; "
-                                   f"use `git machete fork-point {diff_branch} --override-to...`")
 
         self.__git.display_diff(
             # In case no param has been supplied, we want to diff against the current working directory, not the current branch.
@@ -1306,9 +1329,6 @@ class MacheteClient:
 
     def log(self, branch: LocalBranchShortName) -> None:
         fork_point = self.fork_point(branch, use_overrides=True)
-        if not fork_point:
-            raise MacheteException(f"Fork point not found for branch <b>{branch}</b>; "
-                                   f"use `git machete fork-point {branch} --override-to...`")
         self.__git.display_branch_history_from_fork_point(branch.full_name(), fork_point)
 
     def down(self, branch: LocalBranchShortName, pick_mode: bool) -> List[LocalBranchShortName]:
@@ -1421,11 +1441,10 @@ class MacheteClient:
         if self.__git.check_hook_executable(hook_path):
             debug(f"running machete-post-slide-out hook ({hook_path})")
             new_downstreams_strings: List[str] = [str(db) for db in new_downstreams]
-            exit_code = utils.run_cmd(hook_path, new_upstream, slid_out_branch, *new_downstreams_strings,
-                                      cwd=self.__git.get_root_dir())
+            exit_code = self.__run_hook(hook_path, new_upstream, slid_out_branch, *new_downstreams_strings,
+                                        cwd=self.__git.get_root_dir())
             if exit_code != 0:
-                raise MacheteException(
-                    f"The machete-post-slide-out hook exited with {exit_code}, aborting.\n")
+                raise MacheteException(f"The machete-post-slide-out hook exited with {exit_code}, aborting.")
 
     def squash(self, *, current_branch: LocalBranchShortName, opt_fork_point: AnyRevision) -> None:
         commits: List[GitLogEntry] = self.__git.get_commits_between(
@@ -1513,7 +1532,7 @@ class MacheteClient:
                   hash not in hashes_to_exclude and not is_excluded_reflog_subject(hash, gs)]
         reflog = (", ".join(result) or "<empty>")
         debug("computed filtered reflog (= reflog without branch creation "
-              f"and branch reset events irrelevant for fork point/upstream inference): {reflog}\n")
+              f"and branch reset events irrelevant for fork point/upstream inference): {reflog}")
         return result
 
     def sync_annotations_to_github_prs(self) -> None:
@@ -1955,7 +1974,7 @@ class MacheteClient:
             return 'y'
         try:
             ans: str = input(fmt(msg) if apply_fmt else msg).lower()
-        except InterruptedError:
+        except InterruptedError:  # pragma: no cover
             sys.exit(1)
         return ans
 
@@ -2585,7 +2604,11 @@ class MacheteClient:
         self._delete_branches(branches_to_delete=branches_to_delete, opt_yes=opt_yes)
         self.save_definition_file()
 
+    @staticmethod
+    def is_stdout_a_tty() -> bool:
+        return sys.stdout.isatty()
+
     @staticmethod
     def should_perform_interactive_slide_out(cmd: str) -> bool:
         interactive_slide_out_safe_commands = {'traverse', 'status'}
-        return sys.stdout.isatty() and cmd in interactive_slide_out_safe_commands
+        return MacheteClient.is_stdout_a_tty() and cmd in interactive_slide_out_safe_commands
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 6db697c..f2748ce 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -279,7 +279,7 @@ class GitContext:
             # which is irrelevant for our purpose (checking whether certain git CLI features are available/bugs are fixed).
             raw = re.search(r"\d+.\d+.\d+", self._popen_git("version").stdout)
             if not raw:  # unlikely, never observed so far; mostly to satisfy mypy
-                return 0, 0, 0
+                return 0, 0, 0  # pragma: no cover
             self._git_version = tuple(map(int, raw.group(0).split(".")))
         return self._git_version
 
@@ -409,9 +409,9 @@ class GitContext:
             opt_force = []
         elif self.get_git_version() >= (2, 30, 0):  # earliest version of git to support 'push --force-with-lease --force-if-includes'
             opt_force = ["--force-with-lease", "--force-if-includes"]
-        elif self.get_git_version() >= (1, 8, 5):  # earliest version of git to support 'push --force-with-lease'
+        elif self.get_git_version() >= (1, 8, 5):  # pragma: no cover; earliest version of git to support 'push --force-with-lease'
             opt_force = ["--force-with-lease"]
-        else:
+        else:  # pragma: no cover
             opt_force = ["--force"]
         args = [remote, branch]
         self._run_git("push", "--set-upstream", *(opt_force + args))
@@ -667,7 +667,7 @@ class GitContext:
                 self.__load_all_reflogs()
             assert self.__reflogs_cached is not None
             return self.__reflogs_cached.get(branch, [])
-        else:
+        else:  # pragma: no cover
             if self.__reflogs_cached is None:
                 self.__reflogs_cached = {}
             if branch not in self.__reflogs_cached:
@@ -863,7 +863,7 @@ class GitContext:
                     utils.get_non_empty_lines(
                         self._popen_git("for-each-ref", "--format=%(refname)", "--merged", "HEAD", "refs/heads").stdout)))
         else:
-            return list(
+            return list(  # pragma: no cover
                 filter(lambda branch: self.is_ancestor_or_equal(branch, AnyRevision.of('HEAD')),
                        map(lambda branch: LocalBranchFullName.of(branch).to_short_name(),
                            utils.get_non_empty_lines(
@@ -904,55 +904,42 @@ class GitContext:
         self.flush_caches()
 
     def rebase(self, onto: AnyRevision, from_exclusive: AnyRevision, branch: LocalBranchShortName, opt_no_interactive_rebase: bool) -> None:
-        def do_rebase() -> None:
-            # Let's use `OPTS` suffix for consistency with git's built-in env var `GIT_DIFF_OPTS`
-            git_machete_rebase_opts_var = 'GIT_MACHETE_REBASE_OPTS'
-            rebase_opts = os.environ.get(git_machete_rebase_opts_var, '').split()
-            try:
-                if not opt_no_interactive_rebase:
-                    rebase_opts.append("--interactive")
-                if self.get_git_version() >= (2, 26, 0):
-                    rebase_opts.append("--empty=drop")
-                self._run_git("rebase", *rebase_opts, "--onto", onto, from_exclusive, branch)
-            finally:
-                # https://public-inbox.org/git/317468c6-40cc-9f26-8ee3-3392c3908efb@talktalk.net/T
-                # In our case, this can happen when git version invoked by git-machete to start the rebase
-                # is different than git version used (outside of git-machete) to continue the rebase.
-                # This used to be the case when git-machete was installed via a strict-confinement snap
-                # with its own version of git baked in as a dependency.
-                # Currently we're using classic-confinement snaps which no longer have this problem
-                # (snapped git-machete uses whatever git is available in the host system),
-                # but it still doesn't harm to patch the author script.
-
-                # No need to fix <git-dir>/rebase-apply/author-script,
-                # only <git-dir>/rebase-merge/author-script (i.e. interactive rebases, for the most part) is affected.
-                author_script = self.get_worktree_git_subpath("rebase-merge", "author-script")
-                if os.path.isfile(author_script):
-                    faulty_line_regex = re.compile("[A-Z0-9_]+='[^']*")
-
-                    def fix_if_needed(line: str) -> str:
-                        return f"{line.rstrip()}'\n" if faulty_line_regex.fullmatch(line) else line
-
-                    def get_all_lines_fixed() -> Iterator[str]:
-                        with open(author_script) as f_read:
-                            return map(fix_if_needed, f_read.readlines())
-
-                    fixed_lines = get_all_lines_fixed()  # must happen before we open for writing
-                    # See https://github.com/VirtusLab/git-machete/issues/935 for why author-script needs to be saved in this manner
-                    io.open(author_script, "w", newline="").write("".join(fixed_lines))
-                self.flush_caches()
-
-        hook_path = self.get_hook_path("machete-pre-rebase")
-        if self.check_hook_executable(hook_path):
-            debug(f"running machete-pre-rebase hook ({hook_path})")
-            exit_code = utils.run_cmd(hook_path, onto, from_exclusive, branch, cwd=self.get_root_dir())
-            if exit_code == 0:
-                do_rebase()
-            else:
-                raise MacheteException(
-                    f"The machete-pre-rebase hook refused to rebase. Error code: {exit_code}\n")
-        else:
-            do_rebase()
+        # Let's use `OPTS` suffix for consistency with git's built-in env var `GIT_DIFF_OPTS`
+        git_machete_rebase_opts_var = 'GIT_MACHETE_REBASE_OPTS'
+        rebase_opts = os.environ.get(git_machete_rebase_opts_var, '').split()
+        try:
+            if not opt_no_interactive_rebase:
+                rebase_opts.append("--interactive")
+            if self.get_git_version() >= (2, 26, 0):  # pragma: no branch
+                rebase_opts.append("--empty=drop")
+            self._run_git("rebase", *rebase_opts, "--onto", onto, from_exclusive, branch)
+        finally:
+            # https://public-inbox.org/git/317468c6-40cc-9f26-8ee3-3392c3908efb@talktalk.net/T
+            # In our case, this can happen when git version invoked by git-machete to start the rebase
+            # is different than git version used (outside of git-machete) to continue the rebase.
+            # This used to be the case when git-machete was installed via a strict-confinement snap
+            # with its own version of git baked in as a dependency.
+            # Currently we're using classic-confinement snaps which no longer have this problem
+            # (snapped git-machete uses whatever git is available in the host system),
+            # but it still doesn't harm to patch the author script.
+
+            # No need to fix <git-dir>/rebase-apply/author-script,
+            # only <git-dir>/rebase-merge/author-script (i.e. interactive rebases, for the most part) is affected.
+            author_script = self.get_worktree_git_subpath("rebase-merge", "author-script")
+            if os.path.isfile(author_script):
+                faulty_line_regex = re.compile("[A-Z0-9_]+='[^']*")
+
+                def fix_if_needed(line: str) -> str:
+                    return f"{line.rstrip()}'\n" if faulty_line_regex.fullmatch(line) else line
+
+                def get_all_lines_fixed() -> Iterator[str]:
+                    with open(author_script) as f_read:
+                        return map(fix_if_needed, f_read.readlines())
+
+                fixed_lines = get_all_lines_fixed()  # must happen before we open for writing
+                # See https://github.com/VirtusLab/git-machete/issues/935 for why author-script needs to be saved in this manner
+                io.open(author_script, "w", newline="").write("".join(fixed_lines))
+            self.flush_caches()
 
     def rebase_onto_ancestor_commit(self,
                                     branch: LocalBranchShortName,
diff --git a/tox.ini b/tox.ini
index 1134b40..47628c1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,7 @@
 # Let's still include pep8-check despite a preceding autopep8 run to filter out certain errors
 # (like W293 blank line in multiline strings contains whitespace)
 # that don't seem to ever get fixed by autopep8.
-envlist = pep8,isort,pep8-check,mypy
+envlist = pep8,isort,pep8-check,mypy-py311
 minversion = 2.3.2
 skipsdist = True
 
@@ -80,7 +80,7 @@ commands =
   coverage erase
   # Generates .coverage binary file, used as the source of truth for the subsequent reports
   # 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
+  coverage run --branch --omit='tests/*' -m pytest --junitxml=test-results/testenv-{envname}.xml {posargs}
   # Prints a report to console
   coverage report
   # Saves a report to htmlcov/
@@ -93,6 +93,7 @@ exclude_lines =
   except EOFError:
   except KeyboardInterrupt:
   if __name__ == .__main__.:
+  pragma: no cover
 
 # Python 3.11 is needed in envs that use Sphinx due to https://github.com/VirtusLab/git-machete/issues/936
 
@@ -146,7 +147,7 @@ deps =
 commands =
   mypy --config-file mypy.ini git_machete tests
 
-[testenv:mypy]
+[testenv:mypy-py{37,38,39,310,311}]
 deps =
   -r{toxinidir}/requirements.mypy.txt
 commands =


Test patch:
diff --git a/tests/base_test.py b/tests/base_test.py
index 91e814b..dba2e5c 100644
--- a/tests/base_test.py
+++ b/tests/base_test.py
@@ -6,12 +6,6 @@ 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()
 
 
@@ -37,10 +31,18 @@ class GitRepositorySandbox:
         self.local_path = mkdtemp()
         self.file_counter = 0
 
+    def popen(self, command: str) -> str:
+        with os.popen(command) as process:
+            return process.read().strip()
+
     def execute(self, command: str) -> "GitRepositorySandbox":
         subprocess.check_call(command, shell=True)
         return self
 
+    def execute_ignoring_exit_code(self, command: str) -> "GitRepositorySandbox":
+        subprocess.call(command, shell=True)
+        return self
+
     def new_repo(self, directory: str, bare: bool, switch_dir_to_new_repo: bool = True) -> "GitRepositorySandbox":
         previous_dir = os.getcwd()
         os.chdir(directory)
@@ -54,7 +56,7 @@ class GitRepositorySandbox:
         self.execute(f"git checkout -b {branch_name}")
         return self
 
-    def new_root_branch(self, branch_name: str) -> "GitRepositorySandbox":
+    def new_orphan_branch(self, branch_name: str) -> "GitRepositorySandbox":
         self.execute(f"git checkout --orphan {branch_name}")
         return self
 
@@ -82,7 +84,7 @@ class GitRepositorySandbox:
         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")
+        branch = self.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
@@ -108,7 +110,7 @@ class GitRepositorySandbox:
         return self
 
     def get_local_branches(self) -> List[str]:
-        return popen('git for-each-ref refs/heads/ "--format=%(refname:short)"').splitlines()
+        return self.popen('git for-each-ref refs/heads/ "--format=%(refname:short)"').splitlines()
 
     def is_ancestor(self, earlier: str, later: str) -> bool:
         return subprocess.call(f'git merge-base --is-ancestor  "{earlier}"  "{later}"', shell=True) == 0
@@ -117,7 +119,7 @@ class GitRepositorySandbox:
         return self.get_commit_hash("HEAD")
 
     def get_commit_hash(self, revision: str) -> str:
-        return popen(f"git rev-parse {revision}")
+        return self.popen(f"git rev-parse {revision}")
 
     def set_git_config_key(self, key: str, value: str) -> "GitRepositorySandbox":
         self.execute(f'git config {key} {value}')
diff --git a/tests/mockers_github.py b/tests/mockers_github.py
index 8b4b56e..fb6818a 100644
--- a/tests/mockers_github.py
+++ b/tests/mockers_github.py
@@ -1,9 +1,10 @@
+import contextlib
 import json
 import re
 from http import HTTPStatus
 from subprocess import CompletedProcess
 from tempfile import mkdtemp
-from typing import Any, Callable, ContextManager, Dict, List, Optional, Union
+from typing import Any, Callable, Dict, Iterator, List, Optional, Union
 from urllib.error import HTTPError
 from urllib.parse import ParseResult, parse_qs, urlparse
 
@@ -284,24 +285,15 @@ class MockHTTPError(HTTPError):
         return json.dumps(self.msg).encode()
 
 
-class MockContextManager(ContextManager[MockGitHubAPIResponse]):
-    def __init__(self, obj: MockGitHubAPIResponse) -> None:
-        self.obj = obj
+@contextlib.contextmanager
+def mock_urlopen(obj: MockGitHubAPIResponse) -> Iterator[MockGitHubAPIResponse]:
+    if obj.status_code == HTTPStatus.NOT_FOUND:
+        raise HTTPError("http://example.org", 404, 'Not found', None, None)  # type: ignore[arg-type]
+    elif obj.status_code == HTTPStatus.UNPROCESSABLE_ENTITY:
+        raise MockHTTPError("http://example.org", 422, obj.response_data, None, None)  # type: ignore[arg-type]
+    yield 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]
+@contextlib.contextmanager
+def mock_urlopen_raising_403(obj: MockGitHubAPIResponse) -> Iterator[MockGitHubAPIResponse]:
+    raise HTTPError("http://example.org", 403, 'Forbidden', None, None)  # type: ignore[arg-type]
diff --git a/tests/test_add.py b/tests/test_add.py
index 9573ea8..fd096c3 100644
--- a/tests/test_add.py
+++ b/tests/test_add.py
@@ -1,8 +1,9 @@
 from typing import Any
 
 from .base_test import BaseTest
-from .mockers import (assert_failure, assert_success,
-                      mock_run_cmd_and_discard_output, rewrite_definition_file)
+from .mockers import (assert_failure, assert_success, mock_input_returning,
+                      mock_input_returning_y, mock_run_cmd_and_discard_output,
+                      rewrite_definition_file)
 
 
 class TestAdd(BaseTest):
@@ -31,6 +32,11 @@ class TestAdd(BaseTest):
         self.repo_sandbox.new_branch("bugfix/feature_fail")
 
         # Test `git machete add` without providing the branch name
+        mocker.patch("builtins.input", mock_input_returning("n"))
+        assert_success(
+            ['add'],
+            'Add bugfix/feature_fail onto the inferred upstream (parent) branch develop? (y, N) \n'
+        )
         assert_success(
             ['add', '-y'],
             'Adding bugfix/feature_fail onto the inferred upstream (parent) branch develop\n'
@@ -76,12 +82,25 @@ class TestAdd(BaseTest):
             .delete_branch("feature/foo")
         )
 
+        mocker.patch("builtins.input", mock_input_returning("n"))
+        assert_success(
+            ['add', 'foo'],
+            'A local branch foo does not exist. Create out of the current HEAD? (y, N) \n'
+        )
+
         assert_success(
             ['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'
         )
 
+        mocker.patch("builtins.input", mock_input_returning("n"))
+        assert_success(
+            ['add', '--as-root', 'feature/foo'],
+            'A local branch feature/foo does not exist, but a remote branch origin/feature/foo exists.\n'
+            'Check out feature/foo locally? (y, N) \n'
+        )
+
         assert_success(
             ['add', '-y', '--as-root', 'feature/foo'],
             'A local branch feature/foo does not exist, but a remote branch origin/feature/foo exists.\n'
@@ -89,6 +108,43 @@ class TestAdd(BaseTest):
             'Added branch feature/foo as a new root\n'
         )
 
+    def test_add_new_branch_onto_managed_current_branch(self, mocker: Any) -> None:
+        (
+            self.repo_sandbox.new_branch("master")
+            .commit()
+        )
+
+        rewrite_definition_file("master")
+
+        mocker.patch("builtins.input", mock_input_returning_y)
+        assert_success(
+            ['add', 'foo'],
+            "A local branch foo does not exist. Create out of the current HEAD? (y, N) \n"
+            "Added branch foo onto master\n"
+        )
+
+    def test_add_new_branch_when_cannot_infer_parent(self, mocker: Any) -> None:
+        (
+            self.repo_sandbox.new_branch("master")
+            .commit()
+            .new_branch("develop")
+            .commit()
+            .check_out("master")
+        )
+
+        rewrite_definition_file("develop")
+
+        mocker.patch("builtins.input", mock_input_returning_y)
+        assert_failure(
+            ['add', 'foo'],
+            """
+            Could not automatically infer upstream (parent) branch for foo.
+            You can either:
+            1) specify the desired upstream branch with --onto or
+            2) pass --as-root to attach foo as a new root or
+            3) edit the definition file manually with git machete edit"""
+        )
+
     def test_add_already_managed_branch(self) -> None:
         (
             self.repo_sandbox.new_branch("master")
@@ -100,3 +156,19 @@ class TestAdd(BaseTest):
         rewrite_definition_file("master\n  develop")
 
         assert_failure(['add', 'develop'], 'Branch develop already exists in the tree of branch dependencies')
+
+    def test_add_onto_non_existent_branch(self) -> None:
+        (
+            self.repo_sandbox.new_branch("master")
+            .commit("master commit.")
+            .new_branch("develop")
+            .commit("develop commit.")
+        )
+
+        rewrite_definition_file("master")
+
+        assert_failure(
+            ['add', 'develop', '--onto', 'foo'],
+            "Branch foo not found in the tree of branch dependencies.\n"
+            "Use git machete add foo or git machete edit."
+        )
diff --git a/tests/test_delete_unmanaged.py b/tests/test_delete_unmanaged.py
index f871f06..764c47e 100644
--- a/tests/test_delete_unmanaged.py
+++ b/tests/test_delete_unmanaged.py
@@ -26,20 +26,29 @@ class TestDeleteUnmanaged(BaseTest):
             """
         rewrite_definition_file(body)
 
+        mocker.patch("builtins.input", mock_input_returning("q"))
+        launch_command("delete-unmanaged")
+        mocker.patch("builtins.input", mock_input_returning("n"))
+        launch_command("delete-unmanaged")
+        mocker.patch("builtins.input", mock_input_returning("y"))
         assert_success(
-            ["delete-unmanaged", "--yes"],
+            ["delete-unmanaged"],
             """
             Checking for unmanaged branches...
             Skipping current branch feature
-            Deleting branch develop (merged to HEAD)...
+            Delete branch develop (merged to HEAD)? (y, N, q) 
             Deleted branch develop (was 03e727b).
 
             """
         )
 
         self.repo_sandbox.check_out("master")
+        mocker.patch("builtins.input", mock_input_returning("q"))
+        launch_command("delete-unmanaged")
+        mocker.patch("builtins.input", mock_input_returning("n"))
+        launch_command("delete-unmanaged")
         assert_success(
-            ["delete-unmanaged", "-y"],
+            ["delete-unmanaged", "--yes"],
             """
             Checking for unmanaged branches...
             Deleting branch feature (unmerged to HEAD)...
diff --git a/tests/test_fork_point.py b/tests/test_fork_point.py
index bdefb81..8c221d7 100644
--- a/tests/test_fork_point.py
+++ b/tests/test_fork_point.py
@@ -93,6 +93,9 @@ class TestForkPoint(BaseTest):
         self.repo_sandbox.set_git_config_key('machete.overrideForkPoint.develop.to', 40 * '#')
         assert launch_command('fork-point').strip() == develop_branch_fork_point
 
+        # invalid fork-point override revision
+        assert_failure(['fork-point', '--override-to=no-such-commit'], "Cannot find revision no-such-commit")
+
         # valid commit hash but not present in the repository
         self.repo_sandbox.set_git_config_key('machete.overrideForkPoint.develop.to', 40 * 'a')
         assert launch_command('fork-point').strip() == (
@@ -176,3 +179,58 @@ class TestForkPoint(BaseTest):
               o-develop * (untracked)
             """
         )
+
+    def test_fork_point_overridden_to_non_descendant_of_parent_while_branch_descendant_of_parent(self) -> None:
+        with fixed_author_and_committer_date():
+            (
+                self.repo_sandbox
+                .new_branch("branch-0")
+                .commit()
+                .new_branch("branch-1")
+                .commit()
+                .new_branch("branch-2")
+                .commit()
+            )
+
+        body: str = \
+            """
+            branch-1
+                branch-2
+            """
+        rewrite_definition_file(body)
+        launch_command('fork-point', '--override-to=branch-0')
+
+        assert launch_command("fork-point").strip() == self.repo_sandbox.get_commit_hash("branch-1")
+
+    def test_fork_point_while_parent_unrelated_to_child(self) -> None:
+        (
+            self.repo_sandbox
+            .new_branch("branch-1")
+            .commit()
+            .new_orphan_branch("branch-2")
+            .commit()
+        )
+
+        body: str = \
+            """
+            branch-1
+                branch-2
+            """
+        rewrite_definition_file(body)
+
+        assert_failure(
+            ["fork-point"],
+            "Fork point not found for branch branch-2; use git machete fork-point branch-2 --override-to..."
+        )
+
+    def test_fork_point_when_no_other_branches(self) -> None:
+        (
+            self.repo_sandbox
+            .new_branch("branch-1")
+            .commit()
+        )
+
+        assert_failure(
+            ["fork-point"],
+            "Fork point not found for branch branch-1; use git machete fork-point branch-1 --override-to..."
+        )
diff --git a/tests/test_github.py b/tests/test_github.py
index adf3358..3137c78 100644
--- a/tests/test_github.py
+++ b/tests/test_github.py
@@ -10,9 +10,7 @@ from tests.mockers import (assert_failure, assert_success, launch_command,
                            mock_input_returning,
                            mock_run_cmd_and_discard_output,
                            rewrite_definition_file)
-from tests.mockers_github import (FakeCommandLineOptions, MockContextManager,
-                                  MockContextManagerRaise403,
-                                  MockGitHubAPIState,
+from tests.mockers_github import (FakeCommandLineOptions, MockGitHubAPIState,
                                   mock_derive_current_user_login,
                                   mock_for_domain_fake, mock_for_domain_none,
                                   mock_from_url, mock_is_file_false,
@@ -21,7 +19,8 @@ from tests.mockers_github import (FakeCommandLineOptions, MockContextManager,
                                   mock_os_environ_get_github_token,
                                   mock_os_environ_get_none,
                                   mock_repository_info, mock_shutil_which,
-                                  mock_subprocess_run)
+                                  mock_subprocess_run, mock_urlopen,
+                                  mock_urlopen_raising_403)
 
 PRS_PER_PAGE = 3
 NUMBER_OF_PAGES = 3
@@ -81,7 +80,7 @@ class TestGitHub(BaseTest):
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
         mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
         mocker.patch('git_machete.github.GitHubClient.derive_current_user_login', mock_derive_current_user_login)
         mocker.patch('urllib.request.Request', MockGitHubAPIState([]).new_request())
         # TODO (#915): test code should not be mocked
@@ -118,7 +117,7 @@ class TestGitHub(BaseTest):
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
         mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
-        mocker.patch('urllib.request.urlopen', MockContextManagerRaise403)
+        mocker.patch('urllib.request.urlopen', mock_urlopen_raising_403)
 
         github_enterprise_domain = 'git.example.org'
         self.repo_sandbox.set_git_config_key('machete.github.domain', github_enterprise_domain)
@@ -150,7 +149,7 @@ class TestGitHub(BaseTest):
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_fake)
         mocker.patch('git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_enterprise_domain.new_request())
 
         github_enterprise_domain = 'git.example.org'
@@ -173,7 +172,7 @@ class TestGitHub(BaseTest):
         mocker.patch('os.path.isfile', mock_is_file_false)
         mocker.patch('shutil.which', mock_shutil_which(None))
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_enterprise_domain.new_request())
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
 
         (
             self.repo_sandbox.new_branch("develop")
@@ -299,7 +298,7 @@ class TestGitHub(BaseTest):
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
         mocker.patch('urllib.request.Request',
                      self.git_api_state_for_test_local_branch_name_different_than_tracking_branch_name.new_request())
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
 
         (
             self.repo_sandbox.new_branch("root")
diff --git a/tests/test_github_anno_prs.py b/tests/test_github_anno_prs.py
index e3cb47a..3c3dfa3 100644
--- a/tests/test_github_anno_prs.py
+++ b/tests/test_github_anno_prs.py
@@ -4,9 +4,9 @@ from tests.base_test import BaseTest
 from tests.mockers import (assert_success, launch_command,
                            mock_run_cmd_and_discard_output,
                            rewrite_definition_file)
-from tests.mockers_github import (MockContextManager, MockGitHubAPIState,
+from tests.mockers_github import (MockGitHubAPIState,
                                   mock_derive_current_user_login,
-                                  mock_repository_info)
+                                  mock_repository_info, mock_urlopen)
 
 
 class TestGitHubAnnoPRs(BaseTest):
@@ -43,7 +43,7 @@ class TestGitHubAnnoPRs(BaseTest):
     def test_github_anno_prs(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
         mocker.patch('git_machete.github.GitHubClient.derive_current_user_login', mock_derive_current_user_login)
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_anno_prs.new_request())
 
         (
diff --git a/tests/test_github_checkout_prs.py b/tests/test_github_checkout_prs.py
index f43efdc..d676c35 100644
--- a/tests/test_github_checkout_prs.py
+++ b/tests/test_github_checkout_prs.py
@@ -6,11 +6,11 @@ from tests.base_test import BaseTest, GitRepositorySandbox
 from tests.mockers import (assert_failure, assert_success, launch_command,
                            mock_run_cmd_and_discard_output,
                            rewrite_definition_file)
-from tests.mockers_github import (FakeCommandLineOptions, MockContextManager,
-                                  MockGitHubAPIState,
+from tests.mockers_github import (FakeCommandLineOptions, MockGitHubAPIState,
                                   mock_derive_current_user_login,
                                   mock_fetch_ref, mock_for_domain_none,
-                                  mock_from_url, mock_repository_info)
+                                  mock_from_url, mock_repository_info,
+                                  mock_urlopen)
 
 
 class TestGitHubCheckoutPRs(BaseTest):
@@ -100,7 +100,7 @@ class TestGitHubCheckoutPRs(BaseTest):
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
         mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_checkout_prs.new_request())
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
 
         (
             self.repo_sandbox.new_branch("root")
@@ -392,7 +392,7 @@ class TestGitHubCheckoutPRs(BaseTest):
         # due to `git fetch` executed by `checkout-prs` subcommand.
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_checkout_prs_fresh_repo.new_request())
 
         (
@@ -516,7 +516,7 @@ class TestGitHubCheckoutPRs(BaseTest):
         mocker.patch('git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
         # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs_from_fork_with_deleted_repo`
         # due to `git fetch` executed by `checkout-prs` subcommand.
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_checkout_prs_from_fork_with_deleted_repo.new_request())
 
         (
@@ -634,7 +634,7 @@ class TestGitHubCheckoutPRs(BaseTest):
         mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
         mocker.patch('urllib.request.Request',
                      self.git_api_state_for_test_github_checkout_prs_of_current_user_and_other_users.new_request())
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
         mocker.patch('git_machete.github.GitHubClient.derive_current_user_login', mock_derive_current_user_login)
 
         (
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index 9bb2b74..8c3cf14 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -6,10 +6,10 @@ from tests.mockers import (assert_failure, assert_success, launch_command,
                            mock_input_returning, mock_input_returning_y,
                            mock_run_cmd_and_discard_output,
                            rewrite_definition_file)
-from tests.mockers_github import (FakeCommandLineOptions, MockContextManager,
-                                  MockGitHubAPIState, MockHTTPError,
-                                  mock_for_domain_none, mock_from_url,
-                                  mock_repository_info)
+from tests.mockers_github import (FakeCommandLineOptions, MockGitHubAPIState,
+                                  MockHTTPError, mock_for_domain_none,
+                                  mock_from_url, mock_repository_info,
+                                  mock_urlopen)
 
 
 class TestGitHubCreatePR(BaseTest):
@@ -41,7 +41,7 @@ class TestGitHubCreatePR(BaseTest):
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('urllib.error.HTTPError', MockHTTPError)  # need to provide read() method, which does not actually reads error from url
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_create_pr.new_request())
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
 
         (
             self.repo_sandbox.new_branch("root")
@@ -234,7 +234,7 @@ class TestGitHubCreatePR(BaseTest):
         mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_create_pr_missing_base_branch_on_remote.new_request())
 
         (
@@ -302,7 +302,7 @@ class TestGitHubCreatePR(BaseTest):
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('urllib.error.HTTPError', MockHTTPError)  # need to provide read() method, which does not actually read error from url
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_create_pr_with_multiple_non_origin_remotes.new_request())
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
 
         origin_1_remote_path = mkdtemp()
         origin_2_remote_path = mkdtemp()
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index 3d8bdee..5212f59 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -5,8 +5,8 @@ from tests.base_test import BaseTest
 from tests.mockers import (assert_failure, assert_success, launch_command,
                            mock_run_cmd_and_discard_output,
                            rewrite_definition_file)
-from tests.mockers_github import (MockContextManager, MockGitHubAPIState,
-                                  mock_from_url, mock_repository_info)
+from tests.mockers_github import (MockGitHubAPIState, mock_from_url,
+                                  mock_repository_info, mock_urlopen)
 
 
 class TestGitHubRetargetPR(BaseTest):
@@ -43,7 +43,7 @@ class TestGitHubRetargetPR(BaseTest):
     def test_github_retarget_pr(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_retarget_pr.new_request())
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
 
         branchs_first_commit_msg = "First commit on branch."
         branchs_second_commit_msg = "Second commit on branch."
@@ -118,7 +118,7 @@ class TestGitHubRetargetPR(BaseTest):
     def test_github_retarget_pr_explicit_branch(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_retarget_pr_explicit_branch.new_request())
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
 
         branchs_first_commit_msg = "First commit on branch."
         branchs_second_commit_msg = "Second commit on branch."
@@ -199,7 +199,7 @@ class TestGitHubRetargetPR(BaseTest):
         mocker.patch('git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_retarget_pr.new_request())
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
 
         branchs_first_commit_msg = "First commit on branch."
         branchs_second_commit_msg = "Second commit on branch."
diff --git a/tests/test_github_sync.py b/tests/test_github_sync.py
index edfd728..97432ec 100644
--- a/tests/test_github_sync.py
+++ b/tests/test_github_sync.py
@@ -5,9 +5,9 @@ from tests.mockers import (assert_success, launch_command,
                            mock_input_returning_y,
                            mock_run_cmd_and_discard_output,
                            rewrite_definition_file)
-from tests.mockers_github import (FakeCommandLineOptions, MockContextManager,
-                                  MockGitHubAPIState, mock_for_domain_fake,
-                                  mock_from_url, mock_repository_info)
+from tests.mockers_github import (FakeCommandLineOptions, MockGitHubAPIState,
+                                  mock_for_domain_fake, mock_from_url,
+                                  mock_repository_info, mock_urlopen)
 
 
 class TestGitHubSync(BaseTest):
@@ -31,7 +31,7 @@ class TestGitHubSync(BaseTest):
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.github.RemoteAndOrganizationAndRepository.from_url', mock_from_url)
         mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_fake)
-        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('urllib.request.urlopen', mock_urlopen)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_sync.new_request())
 
         (
diff --git a/tests/test_go.py b/tests/test_go.py
index d68bccc..bce8e4c 100644
--- a/tests/test_go.py
+++ b/tests/test_go.py
@@ -5,7 +5,8 @@ import pytest
 from git_machete.exceptions import ExitCode
 
 from .base_test import BaseTest
-from .mockers import (launch_command, mock_run_cmd_and_discard_output,
+from .mockers import (assert_failure, assert_success, launch_command,
+                      mock_input_returning, mock_run_cmd_and_discard_output,
                       rewrite_definition_file)
 
 
@@ -36,15 +37,16 @@ class TestGo(BaseTest):
 
         Verify that 'git machete go up' performs 'git checkout' to the
         parent/upstream branch of the current branch.
-
         """
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
 
         (
-            self.repo_sandbox.new_branch("level-0-branch")
+            self.repo_sandbox
+            .new_branch("level-0-branch")
             .commit()
             .new_branch("level-1-branch")
             .commit()
+            .new_branch("level-2-branch")
         )
         body: str = \
             """
@@ -52,20 +54,29 @@ class TestGo(BaseTest):
                 level-1-branch
             """
         rewrite_definition_file(body)
-        launch_command("go", "up")
 
+        self.repo_sandbox.check_out("level-0-branch")
+        assert_failure(["go", "up"], "Branch level-0-branch has no upstream branch")
+
+        self.repo_sandbox.check_out("level-1-branch")
+        launch_command("go", "up")
         assert 'level-0-branch' == launch_command("show", "current").strip(), \
             ("Verify that 'git machete go up' performs 'git checkout' to "
-             "the parent/upstream branch of the current branch."
-             )
-        # check short command behaviour
+             "the parent/upstream branch of the current branch.")
+
         self.repo_sandbox.check_out("level-1-branch")
         launch_command("g", "u")
-
         assert 'level-0-branch' == launch_command("show", "current").strip(), \
             ("Verify that 'git machete g u' performs 'git checkout' to "
-             "the parent/upstream branch of the current branch."
-             )
+             "the parent/upstream branch of the current branch.")
+
+        self.repo_sandbox.check_out("level-2-branch")
+        assert_success(
+            ["g", "u"],
+            "Warn: branch level-2-branch not found in the tree of branch dependencies; "
+            "the upstream has been inferred to level-1-branch\n"
+        )
+        assert 'level-1-branch' == launch_command("show", "current").strip()
 
     def test_go_down(self, mocker: Any) -> None:
         """Verify behaviour of a 'git machete go down' command.
@@ -77,27 +88,37 @@ class TestGo(BaseTest):
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
 
         (
-            self.repo_sandbox.new_branch("level-0-branch")
+            self.repo_sandbox
+            .new_branch("level-0-branch")
             .commit()
             .new_branch("level-1-branch")
             .commit()
-            .check_out("level-0-branch")
+            .new_branch("level-2a-branch")
+            .commit()
+            .check_out("level-1-branch")
+            .new_branch("level-2b-branch")
+            .commit()
         )
         body: str = \
             """
             level-0-branch
                 level-1-branch
+                    level-2a-branch
+                    level-2b-branch
             """
         rewrite_definition_file(body)
-        launch_command("go", "down")
 
-        assert launch_command("show", "current").strip() == 'level-1-branch'
+        self.repo_sandbox.check_out("level-0-branch")
+        launch_command("go", "down")
+        assert launch_command("show", "current").strip() == "level-1-branch"
 
-        # check short command behaviour
         self.repo_sandbox.check_out("level-0-branch")
         launch_command("g", "d")
+        assert launch_command("show", "current").strip() == "level-1-branch"
 
-        assert launch_command("show", "current").strip() == 'level-1-branch'
+        mocker.patch("builtins.input", mock_input_returning("2"))
+        launch_command("go", "down")
+        assert launch_command("show", "current").strip() == "level-2b-branch"
 
     def test_go_first_root_with_downstream(self, mocker: Any) -> None:
         """Verify behaviour of a 'git machete go first' command.
@@ -105,7 +126,6 @@ class TestGo(BaseTest):
         Verify that 'git machete go first' performs 'git checkout' to
         the first downstream branch of a root branch in the config file
         if root branch has any downstream branches.
-
         """
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
 
@@ -124,11 +144,10 @@ class TestGo(BaseTest):
             .new_branch("level-3b-branch")
             .commit()
             # a added so root will be placed in the config file after the level-0-branch
-            .new_root_branch("a-additional-root")
+            .new_orphan_branch("a-additional-root")
             .commit()
             .new_branch("branch-from-a-additional-root")
             .commit()
-            .check_out("level-3b-branch")
         )
         body: str = \
             """
@@ -137,33 +156,29 @@ class TestGo(BaseTest):
                     level-2a-branch
                 level-1b-branch
                     level-2b-branch
-                        level-3b-branch
             a-additional-root
                 branch-from-a-additional-root
             """
         rewrite_definition_file(body)
-        launch_command("go", "first")
 
+        self.repo_sandbox.check_out("level-3b-branch")
+        launch_command("go", "first")
         assert 'level-1a-branch' == launch_command("show", "current").strip(), \
             ("Verify that 'git machete go first' performs 'git checkout' to "
              "the first downstream branch of a root branch if root branch "
-             "has any downstream branches."
-             )
-        # check short command behaviour
+             "has any downstream branches.")
+
         self.repo_sandbox.check_out("level-3b-branch")
         launch_command("g", "f")
-
         assert 'level-1a-branch' == launch_command("show", "current").strip(), \
             ("Verify that 'git machete g d' performs 'git checkout' to "
-             "the child/downstream branch of the current branch."
-             )
+             "the child/downstream branch of the current branch.")
 
     def test_go_first_root_without_downstream(self, mocker: Any) -> None:
         """Verify behaviour of a 'git machete go first' command.
 
         Verify that 'git machete go first' set current branch to root
         if root branch has no downstream.
-
         """
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
 
@@ -210,12 +225,13 @@ class TestGo(BaseTest):
             .check_out("level-0-branch")
             .new_branch("level-1b-branch")
             .commit()
+            .new_branch("level-2b-branch")
+            .commit()
             # x added so root will be placed in the config file after the level-0-branch
-            .new_root_branch("x-additional-root")
+            .new_orphan_branch("x-additional-root")
             .commit()
             .new_branch("branch-from-x-additional-root")
             .commit()
-            .check_out("level-1a-branch")
         )
         body: str = \
             """
@@ -227,22 +243,24 @@ class TestGo(BaseTest):
                 branch-from-x-additional-root
             """
         rewrite_definition_file(body)
-        launch_command("go", "last")
 
+        self.repo_sandbox.check_out("level-1a-branch")
+        launch_command("go", "last")
         assert 'level-1b-branch' == launch_command("show", "current").strip(), \
             ("Verify that 'git machete go last' performs 'git checkout' to "
              "the last downstream branch of a root branch if root branch "
-             "has any downstream branches."
-             )
-        # check short command behaviour
+             "has any downstream branches.")
+
         self.repo_sandbox.check_out("level-1a-branch")
         launch_command("g", "l")
-
         assert 'level-1b-branch' == launch_command("show", "current").strip(), \
             ("Verify that 'git machete g l' performs 'git checkout' to "
              "the last downstream branch of a root branch if root branch "
-             "has any downstream branches."
-             )
+             "has any downstream branches.")
+
+        self.repo_sandbox.check_out("level-2b-branch")
+        launch_command("g", "l")
+        assert 'branch-from-x-additional-root' == launch_command("show", "current").strip()
 
     def test_go_next_successor_exists(self, mocker: Any) -> None:
         """Verify behaviour of a 'git machete go next' command.
@@ -306,7 +324,7 @@ class TestGo(BaseTest):
             .new_branch("level-1-branch")
             .commit()
             # x added so root will be placed in the config file after the level-0-branch
-            .new_root_branch("x-additional-root")
+            .new_orphan_branch("x-additional-root")
             .commit()
             .check_out("level-1-branch")
         )
@@ -391,7 +409,7 @@ class TestGo(BaseTest):
             self.repo_sandbox.new_branch("level-0-branch")
             .commit()
             # a added so root will be placed in the config file before the level-0-branch
-            .new_root_branch("a-additional-root")
+            .new_orphan_branch("a-additional-root")
             .commit()
             .check_out("level-0-branch")
         )
@@ -435,7 +453,7 @@ class TestGo(BaseTest):
             .check_out("level-0-branch")
             .new_branch("level-1b-branch")
             .commit()
-            .new_root_branch("additional-root")
+            .new_orphan_branch("additional-root")
             .commit()
             .new_branch("branch-from-additional-root")
             .commit()
@@ -465,3 +483,10 @@ class TestGo(BaseTest):
             ("Verify that 'git machete g r' performs 'git checkout' to "
              "the root of the current branch."
              )
+
+    def test_go_root_no_branches(self) -> None:
+        assert_failure(
+            ["g", "root"],
+            "No branches listed in .git/machete; use git machete discover "
+            "or git machete edit, or edit .git/machete manually."
+        )
diff --git a/tests/test_list.py b/tests/test_list.py
index 1c7f48d..3c8a100 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -105,31 +105,29 @@ class TestList(BaseTest):
             expected_output
         )
 
-        expected_output = """
-        feature_0_0_0
-        """
         assert_success(
             ['list', 'slidable-after', 'feature_0_0'],
-            expected_output
+            "feature_0_0_0\n"
+        )
+        assert_success(
+            ['list', 'slidable-after', 'develop'],
+            ""
+        )
+        assert_success(
+            ['list', 'slidable-after', 'feature_0'],
+            ""
         )
 
-        expected_output = """
-        feature_2
-        """
         assert_success(
             ['list', 'unmanaged'],
-            expected_output
+            "feature_2\n"
         )
 
         self.repo_sandbox.check_out("feature_1")
         launch_command('fork-point', '--override-to-inferred')
-
-        expected_output = """
-        feature_1
-        """
         assert_success(
             ['list', 'with-overridden-fork-point'],
-            expected_output
+            "feature_1\n"
         )
 
         launch_command('fork-point', '--unset-override')
diff --git a/tests/test_show.py b/tests/test_show.py
index dc5b0dc..08fed5c 100644
--- a/tests/test_show.py
+++ b/tests/test_show.py
@@ -185,7 +185,7 @@ class TestShow(BaseTest):
             .new_branch("level-3b-branch")
             .commit()
             # a added so root will be placed in the config file after the level-0-branch
-            .new_root_branch("a-additional-root")
+            .new_orphan_branch("a-additional-root")
             .commit()
             .new_branch("branch-from-a-additional-root")
             .commit()
@@ -236,7 +236,7 @@ class TestShow(BaseTest):
             .new_branch("level-1b-branch")
             .commit()
             # x added so root will be placed in the config file after the level-0-branch
-            .new_root_branch("x-additional-root")
+            .new_orphan_branch("x-additional-root")
             .commit()
             .new_branch("branch-from-x-additional-root")
             .commit()
@@ -352,7 +352,7 @@ class TestShow(BaseTest):
             .check_out("level-0-branch")
             .new_branch("level-1b-branch")
             .commit()
-            .new_root_branch("additional-root")
+            .new_orphan_branch("additional-root")
             .commit()
             .new_branch("branch-from-additional-root")
             .commit()
diff --git a/tests/test_slide_out.py b/tests/test_slide_out.py
index 608cbb6..2a91183 100644
--- a/tests/test_slide_out.py
+++ b/tests/test_slide_out.py
@@ -93,9 +93,8 @@ class TestSlideOut(BaseTest):
             """,
         )
 
-        # Slide-out an interior branch with multiple downstreams. (slide_root)
-        # This rebases all the downstreams onto the new upstream. (develop -> [child_a, child_b])
-        launch_command("traverse", "-Wy")
+        # Slide-out an interior branch with multiple downstreams (slide_root).
+        # This rebases all the downstreams onto the new upstream (develop -> [child_a, child_b]).
         launch_command("go", "up")
         launch_command("go", "up")
 
@@ -114,43 +113,29 @@ class TestSlideOut(BaseTest):
                   o-child_b
                     |
                     | child_d_1
-                    o-child_d
+                    o-child_d (diverged from origin)
                 """,
         )
 
-        launch_command("slide-out", "-n")
-
-        assert_success(
-            ["status", "-l"],
-            """
-            develop
-            |
-            | child_a_1
-            o-child_a (diverged from origin)
-            |
-            | child_b_1
-            o-child_b * (diverged from origin)
-              |
-              | child_d_1
-              x-child_d
-            """,
-        )
+        mocker.patch('builtins.input', mock_input_returning_y)
+        launch_command("slide-out", "-n", "--delete", "--merge")
 
-        launch_command("traverse", "-Wy")
         assert_success(
             ["status", "-l"],
             """
             develop
             |
+            | slide_root_1
             | child_a_1
             o-child_a
             |
+            | slide_root_1
             | child_b_1
             o-child_b *
               |
               | child_d_1
-              o-child_d
-            """,
+              o-child_d (diverged from origin)
+            """
         )
 
         # Slide-out and delete a terminal branch (child_d).
@@ -169,53 +154,43 @@ class TestSlideOut(BaseTest):
             """
             develop
             |
+            | slide_root_1
             | child_a_1
             o-child_a
             |
+            | slide_root_1
             | child_b_1
             o-child_b *
             """,
         )
 
-    def test_slide_out_with_valid_down_fork_point(self, mocker: Any) -> None:
+    def test_slide_out_with_post_slide_out_hook(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_forward_output)
 
         (
             self.repo_sandbox.new_branch('branch-0')
-                .commit()
-                .new_branch('branch-1')
-                .commit()
-                .new_branch('branch-2')
-                .commit()
-                .new_branch('branch-3')
-                .commit()
-                .commit('Second commit on branch-3.')
+            .commit()
+            .new_branch('branch-1')
+            .commit()
+            .new_branch('branch-2')
+            .commit()
         )
-        hash_of_second_commit_on_branch_3 = self.repo_sandbox.get_current_commit_hash()
-        self.repo_sandbox.commit("Third commit on branch-3.")
 
         body: str = \
             """
             branch-0
                 branch-1
                     branch-2
-                        branch-3
             """
         rewrite_definition_file(body)
-        launch_command(
-            'slide-out', '-n', 'branch-1', 'branch-2', '-d',
-            hash_of_second_commit_on_branch_3)
 
-        expected_status_output = (
-            """
-            branch-0 (untracked)
-            |
-            | Third commit on branch-3.
-            o-branch-3 * (untracked)
-            """
-        )
+        self.repo_sandbox.write_to_file(".git/hooks/machete-post-slide-out", '#!/bin/sh\necho "$@" > machete-post-slide-out-output')
+        self.repo_sandbox.set_file_executable(".git/hooks/machete-post-slide-out")
+        launch_command("slide-out", "-n", "branch-1")
+        assert "branch-0 branch-1 branch-2" == self.repo_sandbox.read_file("machete-post-slide-out-output").strip()
 
-        assert_success(['status', '-l'], expected_status_output)
+        self.repo_sandbox.write_to_file(".git/hooks/machete-post-slide-out", "#!/bin/sh\nexit 1")
+        assert_failure(["slide-out", "-n", "branch-2"], "The machete-post-slide-out hook exited with 1, aborting.")
 
     def test_slide_out_with_invalid_down_fork_point(self) -> None:
         with fixed_author_and_committer_date():
@@ -247,6 +222,67 @@ class TestSlideOut(BaseTest):
             "Fork point 790f30303cca5e14d58f85c5aa1359a8d4dace8c is not ancestor of or the tip of the branch-3 branch."
         )
 
+    def test_slide_out_with_down_fork_point_and_no_child_of_last_branch(self) -> None:
+        (
+            self.repo_sandbox
+            .new_branch('branch-0')
+            .commit()
+            .new_branch('branch-1')
+            .commit()
+        )
+
+        body: str = \
+            """
+            branch-0
+                branch-1
+            """
+        rewrite_definition_file(body)
+
+        assert_failure(
+            ['slide-out', '-d=@~', 'branch-1'],
+            "Last branch to slide out must have a child branch if option --down-fork-point is passed"
+        )
+
+    def test_slide_out_with_down_fork_point_and_single_child_of_last_branch(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_forward_output)
+
+        (
+            self.repo_sandbox.new_branch('branch-0')
+                .commit()
+                .new_branch('branch-1')
+                .commit()
+                .new_branch('branch-2')
+                .commit()
+                .new_branch('branch-3')
+                .commit()
+                .commit('Second commit on branch-3.')
+        )
+        hash_of_second_commit_on_branch_3 = self.repo_sandbox.get_current_commit_hash()
+        self.repo_sandbox.commit("Third commit on branch-3.")
+
+        body: str = \
+            """
+            branch-0
+                branch-1
+                    branch-2
+                        branch-3
+            """
+        rewrite_definition_file(body)
+        launch_command(
+            'slide-out', '-n', 'branch-1', 'branch-2', '-d',
+            hash_of_second_commit_on_branch_3)
+
+        expected_status_output = (
+            """
+            branch-0 (untracked)
+            |
+            | Third commit on branch-3.
+            o-branch-3 * (untracked)
+            """
+        )
+
+        assert_success(['status', '-l'], expected_status_output)
+
     def test_slide_out_with_down_fork_point_and_multiple_children_of_last_branch(self) -> None:
         (
             self.repo_sandbox.new_branch('branch-0')
@@ -273,5 +309,49 @@ class TestSlideOut(BaseTest):
 
         assert_failure(
             ['slide-out', '-n', 'branch-1', '-d', hash_of_only_commit_on_branch_2b],
-            "Last branch to slide out can't have more than one child branch if option --down-fork-point is passed."
+            "Last branch to slide out can't have more than one child branch if option --down-fork-point is passed"
+        )
+
+    def test_slide_out_with_invalid_sequence_of_branches(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_forward_output)
+
+        (
+            self.repo_sandbox.new_branch('branch-0')
+                .commit()
+                .new_branch('branch-1')
+                .commit()
+                .new_branch('branch-2a')
+                .commit()
+                .new_branch('branch-3')
+                .commit()
+                .check_out('branch-1')
+                .new_branch('branch-2b')
+                .commit()
+        )
+
+        body: str = \
+            """
+            branch-0
+                branch-1
+                    branch-2a
+                        branch-3
+                    branch-2b
+            """
+        rewrite_definition_file(body)
+
+        assert_failure(
+            ['slide-out', 'branch-0'],
+            "No upstream branch defined for branch-0, cannot slide out"
+        )
+        assert_failure(
+            ['slide-out', 'branch-3', 'branch-2a'],
+            "No downstream branch defined for branch-3, cannot slide out"
+        )
+        assert_failure(
+            ['slide-out', 'branch-2a', 'branch-1'],
+            "branch-1 is not downstream of branch-2a, cannot slide out"
+        )
+        assert_failure(
+            ['slide-out', 'branch-1', 'branch-2a'],
+            "Multiple downstream branches defined for branch-1: branch-2a, branch-2b; cannot slide out"
         )
diff --git a/tests/test_squash.py b/tests/test_squash.py
index 6e503db..915b838 100644
--- a/tests/test_squash.py
+++ b/tests/test_squash.py
@@ -1,5 +1,5 @@
 
-from .base_test import BaseTest, popen
+from .base_test import BaseTest
 from .mockers import (assert_failure, assert_success,
                       fixed_author_and_committer_date, launch_command)
 
@@ -58,7 +58,7 @@ class TestSquash(BaseTest):
             "First commit."
         )
 
-        current_branch_log = popen('git log -3 --format=%s')
+        current_branch_log = self.repo_sandbox.popen('git log -3 --format=%s')
         assert current_branch_log == expected_branch_log, \
             ("Verify that `git machete squash -f <fork-point>` squashes commit"
              " from one succeeding the fork-point until tip of the branch.")
diff --git a/tests/test_status.py b/tests/test_status.py
index 0cb5fa8..642b19e 100644
--- a/tests/test_status.py
+++ b/tests/test_status.py
@@ -1,13 +1,18 @@
 import os
+import re
 import sys
+import textwrap
 from tempfile import mkdtemp
 from typing import Any
 
 import pytest
 
 from .base_test import BaseTest
-from .mockers import (assert_failure, assert_success, launch_command,
-                      mock_run_cmd_and_discard_output, rewrite_definition_file)
+from .mockers import (assert_failure, assert_success,
+                      fixed_author_and_committer_date, launch_command,
+                      mock_input_returning, mock_input_returning_y,
+                      mock_run_cmd_and_discard_output, overridden_environment,
+                      rewrite_definition_file)
 
 
 class TestStatus(BaseTest):
@@ -53,6 +58,71 @@ class TestStatus(BaseTest):
                                       'Edit the definition file manually with git machete edit'
         assert_failure(['status'], expected_error_message)
 
+    def test_single_invalid_branch_interactive_slide_out(self, mocker: Any) -> None:
+        mocker.patch("git_machete.client.MacheteClient.is_stdout_a_tty", lambda: True)
+
+        (
+            self.repo_sandbox
+            .remove_remote()
+            .new_branch('master')
+            .commit()
+        )
+        body: str = \
+            """
+            master
+            \t\tfoo
+            """
+        rewrite_definition_file(body)
+        expected_output = """
+            Skipping foo which is not a local branch (perhaps it has been deleted?).
+            Slide it out from the definition file? (y, e[dit], N) 
+              master *
+        """
+
+        mocker.patch("builtins.input", mock_input_returning(""))
+        assert_success(["status"], expected_output)
+
+        mocker.patch("builtins.input", mock_input_returning("e"))
+        self.repo_sandbox.set_git_config_key("advice.macheteEditorSelection", "false")
+        with overridden_environment(GIT_EDITOR="sed -i.bak '/foo/ d'"):
+            assert_success(["status"], expected_output)
+
+    def test_multiple_invalid_branches_interactive_slide_out(self, mocker: Any) -> None:
+        mocker.patch("git_machete.client.MacheteClient.is_stdout_a_tty", lambda: True)
+
+        (
+            self.repo_sandbox
+            .remove_remote()
+            .new_branch('master')
+            .commit()
+            .new_branch('develop')
+            .commit()
+            .new_branch('feature')
+            .commit()
+        )
+        body: str = \
+            """
+            master
+            \t\tfoo
+            \t\t\t\tbar  PR #1
+            \t\tqux
+            \t\t\t\tdevelop
+            baz
+            \t\tfeature
+            """
+        rewrite_definition_file(body)
+        expected_output = """
+            Skipping foo, bar, qux, baz which are not local branches (perhaps they have been deleted?).
+            Slide them out from the definition file? (y, e[dit], N) 
+              master
+              |
+              o-develop
+
+              feature *
+        """
+        mocker.patch("builtins.input", mock_input_returning_y)
+        assert_success(["status"], expected_output)
+
     @pytest.mark.skipif(sys.platform == "win32", reason="Windows doesn't distinguish between executable and non-executable files")
     def test_status_advice_ignored_non_executable_hook(self) -> None:
         (
@@ -106,6 +176,7 @@ class TestStatus(BaseTest):
         body: str = \
             """
             master
+
               develop
             """
         rewrite_definition_file(body)
@@ -427,8 +498,6 @@ class TestStatus(BaseTest):
         assert_success(['status'], expected_status_output)
 
     def test_status_fork_point_without_reflogs(self, mocker: Any) -> None:
-        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
-
         (
             self.repo_sandbox
             .remove_remote()
@@ -457,3 +526,212 @@ class TestStatus(BaseTest):
             """
         )
         assert_success(['status', '-l'], expected_status_output)
+
+    def test_status_yellow_edges(self) -> None:
+        with fixed_author_and_committer_date():
+            (
+                self.repo_sandbox
+                .remove_remote()
+                .new_branch("master")
+                .commit()
+                .new_branch("develop")
+                .commit()
+                .new_branch("feature-1")
+                .commit()
+                .check_out("develop")
+                .new_branch("feature-2")
+                .commit()
+            )
+        body: str = \
+            """
+            master
+                feature-1
+                feature-2
+            """
+        rewrite_definition_file(body)
+
+        expected_status_output = (
+            """
+              master
+              |
+              ?-feature-1
+              |
+              ?-feature-2 *
+
+            Warn: yellow edges indicate that fork points for feature-1, feature-2 are probably incorrectly inferred,
+            or that some extra branch should be added between each of these branches and its parent.
+
+            Run git machete status --list-commits or git machete status --list-commits-with-hashes to see more details.
+            """
+        )
+        assert_success(['status'], expected_status_output)
+
+        expected_status_output = (
+            """
+              master
+              |
+              | Some commit message. -> fork point ??? commit dcd2db5 seems to be a part of the unique history of develop
+              | Some commit message.
+              ?-feature-1
+              |
+              | Some commit message. -> fork point ??? commit dcd2db5 seems to be a part of the unique history of develop
+              | Some commit message.
+              ?-feature-2 *
+
+            Warn: yellow edges indicate that fork points for feature-1, feature-2 are probably incorrectly inferred,
+            or that some extra branch should be added between each of these branches and its parent.
+
+            Consider using git machete fork-point --override-to=<revision>|--override-to-inferred|--override-to-parent <branch> for each affected branch,
+            or reattaching the affected branches under different parent branches.
+            """  # noqa: E501
+        )
+        assert_success(['status', '-l'], expected_status_output)
+
+    def test_status_non_ascii_junctions(self) -> None:
+        (
+            self.repo_sandbox
+            .remove_remote()
+            .new_branch("develop")
+            .commit()
+            .new_branch("feature-1")
+            .commit()
+            .check_out("develop")
+            .new_branch("feature-2")
+            .commit()
+        )
+
+        body: str = \
+            """
+            develop
+                feature-1
+                feature-2
+            """
+        rewrite_definition_file(body)
+
+        expected_status_output = (
+            """\
+              develop
+              │
+              ├─feature-1
+              │
+              └─feature-2
+            """
+        )
+        raw_output = launch_command('status', '--color=always')
+        assert textwrap.dedent(re.sub('\x1b\\[[^m]+m', '', raw_output)) == textwrap.dedent(expected_status_output)
+
+    def test_status_during_rebase(self) -> None:
+        (
+            self.repo_sandbox
+            .remove_remote()
+            .new_branch("master")
+            .commit()
+            .new_branch("develop")
+            .commit()
+        )
+
+        body: str = \
+            """
+            master
+                develop
+            """
+        rewrite_definition_file(body)
+
+        with overridden_environment(GIT_SEQUENCE_EDITOR="sed -i.bak '1s/^pick /edit /'"):
+            launch_command("update")
+
+        expected_status_output = (
+            """
+            master
+            |
+            | Some commit message.
+            o-REBASING develop *
+            """
+        )
+        assert_success(['status', '-l'], expected_status_output)
+
+    def test_status_during_side_effecting_operations(self) -> None:
+        (
+            self.repo_sandbox
+            .remove_remote()
+            .new_branch("master")
+            .commit()
+            .new_branch("develop")
+            .add_file_and_commit("1.txt", "some-content")
+            .check_out("master")
+            .new_branch("feature")
+            .add_file_and_commit("1.txt", "some-other-content")
+            .check_out("develop")
+        )
+
+        body: str = \
+            """
+            master
+                develop
+            """
+        rewrite_definition_file(body)
+
+        # AM
+
+        patch_path = self.repo_sandbox.popen("git format-patch feature")
+        self.repo_sandbox.execute_ignoring_exit_code(f"git am {patch_path}")
+
+        expected_status_output = (
+            """
+            master
+            |
+            | Some commit message.
+            o-GIT AM IN PROGRESS develop *
+            """
+        )
+        assert_success(['status', '-l'], expected_status_output)
+
+        self.repo_sandbox.execute("git am --abort")
+
+        # CHERRY-PICK
+
+        self.repo_sandbox.execute_ignoring_exit_code("git cherry-pick feature")
+
+        expected_status_output = (
+            """
+            master
+            |
+            | Some commit message.
+            o-CHERRY-PICKING develop *
+            """
+        )
+        assert_success(['status', '-l'], expected_status_output)
+
+        self.repo_sandbox.execute("git cherry-pick --abort")
+
+        # MERGE
+
+        self.repo_sandbox.execute_ignoring_exit_code("git merge feature")
+
+        expected_status_output = (
+            """
+            master
+            |
+            | Some commit message.
+            o-MERGING develop *
+            """
+        )
+        assert_success(['status', '-l'], expected_status_output)
+
+        self.repo_sandbox.execute("git merge --abort")
+
+        # REVERT
+
+        self.repo_sandbox.execute("git revert --no-commit HEAD")
+
+        expected_status_output = (
+            """
+            master
+            |
+            | Some commit message.
+            o-REVERTING develop *
+            """
+        )
+        assert_success(['status', '-l'], expected_status_output)
+
+        self.repo_sandbox.execute("git revert --abort")
diff --git a/tests/test_traverse.py b/tests/test_traverse.py
index c6fdc42..e6b3639 100644
--- a/tests/test_traverse.py
+++ b/tests/test_traverse.py
@@ -2,7 +2,8 @@ from typing import Any
 
 from .base_test import BaseTest, git
 from .mockers import (assert_failure, assert_success, launch_command,
-                      mock_run_cmd_and_discard_output, rewrite_definition_file)
+                      mock_input_returning, mock_run_cmd_and_discard_output,
+                      rewrite_definition_file)
 
 
 class TestTraverse(BaseTest):
@@ -81,6 +82,49 @@ class TestTraverse(BaseTest):
             """
         )
 
+    def test_traverse_slide_out(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
+        (
+            self.repo_sandbox
+            .remove_remote()
+            .new_branch("master")
+            .commit()
+            .new_branch("develop")
+            .commit()
+            .new_branch("feature")
+            .commit()
+            .check_out("master")
+            .execute("git merge --ff-only develop")
+            .check_out("develop")
+        )
+        body: str = \
+            """
+            master
+                develop  PR #123
+                    feature
+            """
+        rewrite_definition_file(body)
+
+        mocker.patch('builtins.input', mock_input_returning("q"))
+        assert_success(
+            ["traverse"],
+            "Branch develop is merged into master. Slide develop out of the tree of branch dependencies? (y, N, q, yq) \n"
+        )
+        mocker.patch('builtins.input', mock_input_returning("yq"))
+        assert_success(
+            ["traverse"],
+            "Branch develop is merged into master. Slide develop out of the tree of branch dependencies? (y, N, q, yq) \n"
+        )
+        assert_success(
+            ["status", "-l"],
+            """
+            master
+            |
+            | Some commit message.
+            o-feature
+            """
+        )
+
     def test_traverse_no_remotes(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
         self.setup_standard_tree()
diff --git a/tests/test_update.py b/tests/test_update.py
index 69067ed..b3092d7 100644
--- a/tests/test_update.py
+++ b/tests/test_update.py
@@ -1,9 +1,12 @@
 from typing import Any
 
-from .base_test import BaseTest, git, popen
-from .mockers import (assert_failure, fixed_author_and_committer_date,
-                      launch_command, mock_run_cmd_and_discard_output,
-                      overridden_environment, rewrite_definition_file)
+from .base_test import BaseTest, git
+from .mockers import (assert_failure, assert_success,
+                      fixed_author_and_committer_date, launch_command,
+                      mock_input_returning, mock_input_returning_y,
+                      mock_run_cmd_and_discard_output,
+                      mock_run_cmd_and_forward_output, overridden_environment,
+                      rewrite_definition_file)
 
 
 class TestUpdate(BaseTest):
@@ -114,7 +117,7 @@ class TestUpdate(BaseTest):
         new_fork_point_hash = launch_command("fork-point").strip()
         assert parents_new_commit_hash == new_fork_point_hash, \
             "Verify that 'git machete update' drops effectively-empty commits."
-        branch_history = popen('git log level-0-branch..level-1-branch')
+        branch_history = self.repo_sandbox.popen('git log level-0-branch..level-1-branch')
         assert "level-1 commit" in branch_history
         assert "level-1 commit... but to be cherry-picked onto level-0-branch" not in branch_history
 
@@ -153,7 +156,7 @@ class TestUpdate(BaseTest):
         launch_command(
             "update", "--no-interactive-rebase", "-f", branch_second_commit_hash)
         new_fork_point_hash = launch_command("fork-point").strip()
-        branch_history = popen('git log -10 --oneline')
+        branch_history = self.repo_sandbox.popen('git log -10 --oneline')
 
         assert roots_second_commit_hash == new_fork_point_hash, \
             ("Verify that 'git machete update --no-interactive-rebase -f "
@@ -203,7 +206,8 @@ class TestUpdate(BaseTest):
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
 
         (
-            self.repo_sandbox.new_branch('branch-0')
+            self.repo_sandbox
+            .new_branch('branch-0')
             .commit()
             .new_branch("branch-1")
             .commit()
@@ -218,3 +222,73 @@ class TestUpdate(BaseTest):
         self.repo_sandbox.execute("git add bar.txt")
         with overridden_environment(GIT_EDITOR="cat"):
             self.repo_sandbox.execute("git rebase --continue")
+
+    def test_update_unmanaged_branch(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
+
+        (
+            self.repo_sandbox
+            .new_branch('branch-0')
+            .commit()
+            .new_branch("branch-1")
+            .commit()
+            .check_out("branch-0")
+            .commit()
+            .check_out("branch-1")
+        )
+        rewrite_definition_file("branch-0")
+
+        original_branch_1_hash = self.repo_sandbox.get_commit_hash("branch-1")
+        mocker.patch("builtins.input", mock_input_returning(""))
+        assert_failure(["update", "--no-interactive-rebase"], "Aborting.")
+        assert self.repo_sandbox.get_commit_hash("branch-1") == original_branch_1_hash
+
+        mocker.patch("builtins.input", mock_input_returning_y)
+        assert_success(
+            ["update", "--no-interactive-rebase"],
+            "Branch branch-1 not found in the tree of branch dependencies. "
+            "Rebase onto the inferred upstream branch-0? (y, N) \n")
+        assert self.repo_sandbox.is_ancestor("branch-0", "branch-1")
+
+    def test_update_unmanaged_branch_when_parent_cannot_be_inferred(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_discard_output)
+
+        (
+            self.repo_sandbox
+            .new_branch('branch-0')
+            .commit()
+            .new_orphan_branch("branch-1")
+            .commit()
+        )
+
+        assert_failure(
+            ["update", "--no-interactive-rebase"],
+            "Branch branch-1 not found in the tree of branch dependencies and its upstream could not be inferred"
+        )
+
+    def test_update_with_pre_rebase_hook(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd_and_forward_output)
+
+        with fixed_author_and_committer_date():
+            (
+                self.repo_sandbox.new_branch('branch-0')
+                .commit()
+                .new_branch('branch-1')
+                .commit()
+            )
+
+        body: str = \
+            """
+            branch-0
+                branch-1
+            """
+        rewrite_definition_file(body)
+
+        self.repo_sandbox.write_to_file(".git/hooks/machete-pre-rebase", '#!/bin/sh\necho "$@" > machete-pre-rebase-output')
+        self.repo_sandbox.set_file_executable(".git/hooks/machete-pre-rebase")
+        launch_command("update", "-n")
+        assert self.repo_sandbox.read_file("machete-pre-rebase-output").strip() == \
+            "refs/heads/branch-0 e6e38425289e69c1c47dc955fdf7f4e8836aa7c4 branch-1"
+
+        self.repo_sandbox.write_to_file(".git/hooks/machete-pre-rebase", "#!/bin/sh\nexit 1")
+        assert_failure(["update", "-n"], "The machete-pre-rebase hook refused to rebase. Error code: 1")