Repository: VirtusLab/git-machete
Pull request #229 (2021-09-13 10:43:12)

Issue #158: Migrate CLI to argparse.

PawelLipski: There's quite a lot of stray commits from Piotr's PRs on this branch (when comparing to develop) 🤔 
BartoszSiwinski: > There's quite a lot of stray commits from Piotr's PRs on this branch (when comparing to develop) 🤔

I think I messed up rebase. I rebased to develop without first rebasing enchance/checkout-pr so it blew up.
PawelLipski: Yeah... if you could `git rebase --onto` or `git machete update --fork-point`

Patch:
diff --git a/git_machete/cli.py b/git_machete/cli.py
index a3d7d33..3290608 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -1,16 +1,16 @@
 #!/usr/bin/env python
 
-import getopt
+import argparse
 import os
 import re
 import sys
 import textwrap
-from typing import Dict, List, Optional, Tuple, TypeVar
+from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
 
 import git_machete.options
 from git_machete import __version__
 from git_machete import utils
-from git_machete.client import MacheteClient, allowed_directions
+from git_machete.client import MacheteClient
 from git_machete.constants import EscapeCodes
 from git_machete.docs import short_docs, long_docs
 from git_machete.exceptions import MacheteException, StopTraversal
@@ -28,7 +28,6 @@ alias_by_command: Dict[str, str] = {
     "log": "l",
     "status": "s"
 }
-command_by_alias: Dict[str, str] = {v: k for k, v in alias_by_command.items()}
 
 command_groups: List[Tuple[str, List[str]]] = [
     # 'is-managed' is mostly for scripting use and therefore skipped
@@ -45,304 +44,513 @@ command_groups: List[Tuple[str, List[str]]] = [
 ]
 
 
-def usage(command: str = None) -> None:
-    if command and command in command_by_alias:
-        command = command_by_alias[command]
+def get_help_description(command: str = None) -> str:
+    usage_str = ''
     if command and command in long_docs:
-        print(fmt(textwrap.dedent(long_docs[command])))
+        usage_str += fmt(textwrap.dedent(long_docs[command]))
     else:
-        print()
-        short_usage()
+        usage_str += get_short_general_usage() + '\n'
         if command:
-            print(f"\nUnknown command: '{command}'")
-        print(fmt("\n<u>TL;DR tip</u>\n\n"
-                  "    Get familiar with the help for <b>format</b>, <b>edit</b>,"
-                  " <b>status</b> and <b>update</b>, in this order.\n"))
+            usage_str += f"\nUnknown command: '{command}'"
+        usage_str += fmt(
+            "\n<u>TL;DR tip</u>\n\n"
+            "    Get familiar with the help for <b>format</b>, <b>edit</b>,"
+            " <b>status</b> and <b>update</b>, in this order.\n\n")
         for hdr, cmds in command_groups:
-            print(underline(hdr))
-            print()
+            usage_str += underline(hdr) + '\n\n'
             for cm in cmds:
                 alias = f", {alias_by_command[cm]}" if cm in alias_by_command else ""
-                print("    %s%-18s%s%s" % (EscapeCodes.BOLD, cm + alias, EscapeCodes.ENDC, short_docs[
-                    cm]))  # bold(...) can't be used here due to the %-18s format specifier
-            sys.stdout.write("\n")
-        print(fmt(textwrap.dedent("""
+                usage_str += ("    %s%-18s%s%s" % (
+                    # bold(...) can't be used here due to the %-18s format specifier
+                    EscapeCodes.BOLD, cm + alias, EscapeCodes.ENDC, short_docs[cm]))
+                usage_str += '\n'
+            usage_str += '\n'
+        usage_str += fmt(textwrap.dedent("""
             <u>General options</u>\n
                 <b>--debug</b>           Log detailed diagnostic info, including outputs of the executed git commands.
                 <b>-h, --help</b>        Print help and exit.
                 <b>-v, --verbose</b>     Log the executed git commands.
                 <b>--version</b>         Print version and exit.
-        """[1:])))
+        """[1:]))
+    return usage_str
 
 
-def short_usage() -> None:
-    print(
-        fmt("<b>Usage: git machete [--debug] [-h] [-v|--verbose] [--version] "
-            "<command> [command-specific options] [command-specific argument]</b>"))
+def get_short_general_usage() -> str:
+    return (fmt("<b>Usage: git machete [--debug] [-h] [-v|--verbose] [--version] "
+                "<command> [command-specific options] [command-specific argument]</b>"))
 
 
 def version() -> None:
     print(f"git-machete version {__version__}")
 
 
-def main() -> None:
-    launch(sys.argv[1:])
-
-
-def launch(orig_args: List[str]) -> None:
-    cli_opts = git_machete.options.CommandLineOptions()
-    git = GitContext(cli_opts)
-
-    if sys.version_info.major == 2 or (sys.version_info.major == 3 and sys.version_info.minor < 6):
+class MacheteHelpAction(argparse.Action):
+    def __init__(
+            self,
+            option_strings: str,
+            dest: str = argparse.SUPPRESS,
+            default: Any = argparse.SUPPRESS,
+            help: str = None
+    ) -> None:
+        super(MacheteHelpAction, self).__init__(
+            option_strings=option_strings,
+            dest=dest,
+            default=default,
+            nargs=0,
+            help=help)
+
+    def __call__(
+            self,
+            parser: argparse.ArgumentParser,
+            namespace: argparse.Namespace,
+            values: Union[str, Sequence[Any], None],
+            option_string: str = None
+    ) -> None:
+        # parser name (prog) is expected to be `git machete` or `git machete <command>`
+        command_name = parser.prog.replace('git machete', '').strip()
+        print(get_help_description(command_name))
+        parser.exit()
+
+
+def create_cli_parser() -> argparse.ArgumentParser:
+
+    common_args_parser = argparse.ArgumentParser(
+        prog='git machete', argument_default=argparse.SUPPRESS, add_help=False)
+    common_args_parser.add_argument('--debug', action='store_true')
+    common_args_parser.add_argument('-h', '--help', action=MacheteHelpAction)
+    common_args_parser.add_argument(
+        '--version', action='version', version=f'%(prog)s version {__version__}')
+    common_args_parser.add_argument('-v', '--verbose', action='store_true')
+
+    cli_parser = argparse.ArgumentParser(
+        prog='git machete',
+        argument_default=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser]
+    )
+
+    subparsers = cli_parser.add_subparsers(dest='command')
+
+    add_parser = subparsers.add_parser(
+        'add',
+        argument_default=argparse.SUPPRESS,
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    add_parser.add_argument('branch', nargs='?')
+    add_parser.add_argument('-o', '--onto')
+    add_parser.add_argument('-R', '--as-root', action='store_true')
+    add_parser.add_argument('-y', '--yes', action='store_true')
+
+    advance_parser = subparsers.add_parser(
+        'advance', usage=argparse.SUPPRESS, add_help=False, parents=[common_args_parser])
+    advance_parser.add_argument('-y', '--yes', action='store_true', default=argparse.SUPPRESS)
+
+    anno_parser = subparsers.add_parser(
+        'anno', usage=argparse.SUPPRESS, add_help=False, parents=[common_args_parser])
+    # possible values of 'annotation_text' include: [], [''], ['some_val'], ['text_1', 'text_2']
+    anno_parser.add_argument('annotation_text', nargs='*')
+    anno_parser.add_argument('-b', '--branch', default=argparse.SUPPRESS)
+    anno_parser.add_argument(
+        '-H', '--sync-github-prs', action='store_true', default=argparse.SUPPRESS)
+
+    delete_unmanaged_parser = subparsers.add_parser(
+        'delete-unmanaged',
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    delete_unmanaged_parser.add_argument(
+        '-y', '--yes', action='store_true', default=argparse.SUPPRESS)
+
+    diff_full_parser = subparsers.add_parser(
+        'diff',
+        aliases=['d'],
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    diff_full_parser.add_argument('branch', nargs='?')
+    diff_full_parser.add_argument(
+        '-s', '--stat', action='store_true', default=argparse.SUPPRESS)
+
+    discover_parser = subparsers.add_parser(
+        'discover',
+        argument_default=argparse.SUPPRESS,
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    discover_parser.add_argument('-C', '--checked-out-since')
+    discover_parser.add_argument('-l', '--list-commits', action='store_true')
+    discover_parser.add_argument('-r', '--roots')
+    discover_parser.add_argument('-y', '--yes', action='store_true')
+
+    subparsers.add_parser(
+        'edit',
+        aliases=['e'],
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+
+    subparsers.add_parser(
+        'file',
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+
+    fork_point_parser = subparsers.add_parser(
+        'fork-point',
+        argument_default=argparse.SUPPRESS,
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    fork_point_parser.add_argument('branch', nargs='?')
+    fork_point_exclusive_optional_args = fork_point_parser.add_mutually_exclusive_group()
+    fork_point_exclusive_optional_args.add_argument('--inferred', action='store_true')
+    fork_point_exclusive_optional_args.add_argument('--override-to')
+    fork_point_exclusive_optional_args.add_argument('--override-to-inferred', action='store_true')
+    fork_point_exclusive_optional_args.add_argument('--override-to-parent', action='store_true')
+    fork_point_exclusive_optional_args.add_argument('--unset-override', action='store_true')
+
+    subparsers.add_parser(
+        'format', add_help=False, usage=argparse.SUPPRESS, parents=[common_args_parser])
+
+    github_parser = subparsers.add_parser(
+        'github',
+        argument_default=argparse.SUPPRESS,
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    github_parser.add_argument(
+        'subcommand', choices=['anno-prs', 'checkout-prs', 'create-pr', 'retarget-pr'])
+    github_parser.add_argument('pr_no', nargs='?')
+    github_parser.add_argument('--draft', action='store_true')
+
+    go_parser = subparsers.add_parser(
+        'go',
+        aliases=['g'],
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    go_parser.add_argument(
+        'direction',
+        choices=['d', 'down', 'f', 'first', 'l', 'last', 'n', 'next',
+                 'p', 'prev', 'r', 'root', 'u', 'up']
+    )
+
+    help_parser = subparsers.add_parser(
+        'help', add_help=False, usage=argparse.SUPPRESS, parents=[common_args_parser])
+    help_parser.add_argument('topic_or_cmd', nargs='?', choices=list(short_docs.keys()))
+
+    subparsers.add_parser(
+        'hooks',
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+
+    is_managed_parser = subparsers.add_parser(
+        'is-managed',
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    is_managed_parser.add_argument('branch', nargs='?')
+
+    list_parser = subparsers.add_parser(
+        'list', usage=argparse.SUPPRESS, add_help=False, parents=[common_args_parser])
+    list_parser.add_argument(
+        'category',
+        choices=[
+            'addable', 'managed', 'slidable', 'slidable-after', 'unmanaged',
+            'with-overridden-fork-point']
+    )
+    list_parser.add_argument('branch', nargs='?', default=argparse.SUPPRESS)
+
+    log_parser = subparsers.add_parser(
+        'log',
+        aliases=['l'],
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    log_parser.add_argument('branch', nargs='?', default=argparse.SUPPRESS)
+
+    reapply_parser = subparsers.add_parser(
+        'reapply', usage=argparse.SUPPRESS, add_help=False, parents=[common_args_parser])
+    reapply_parser.add_argument('-f', '--fork-point', default=argparse.SUPPRESS)
+
+    show_parser = subparsers.add_parser(
+        'show', usage=argparse.SUPPRESS, add_help=False, parents=[common_args_parser])
+    show_parser.add_argument(
+        'direction',
+        choices=['c', 'current', 'd', 'down', 'f', 'first', 'l', 'last',
+                 'n', 'next', 'p', 'prev', 'r', 'root', 'u', 'up']
+    )
+    show_parser.add_argument('branch', nargs='?', default=argparse.SUPPRESS)
+
+    slide_out_parser = subparsers.add_parser(
+        'slide-out',
+        argument_default=argparse.SUPPRESS,
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    slide_out_parser.add_argument('branches', nargs='*')
+    slide_out_parser.add_argument('-d', '--down-fork-point')
+    slide_out_parser.add_argument('-M', '--merge', action='store_true')
+    slide_out_parser.add_argument('-n', action='store_true')
+    slide_out_parser.add_argument('--no-edit-merge', action='store_true')
+    slide_out_parser.add_argument('--no-interactive-rebase', action='store_true')
+
+    squash_parser = subparsers.add_parser(
+        'squash', usage=argparse.SUPPRESS, add_help=False, parents=[common_args_parser])
+    squash_parser.add_argument('-f', '--fork-point', default=argparse.SUPPRESS)
+
+    status_parser = subparsers.add_parser(
+        'status',
+        aliases=['s'],
+        argument_default=argparse.SUPPRESS,
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    status_parser.add_argument('--color', choices=['always', 'auto', 'never'], default='auto')
+    status_parser.add_argument('-l', '--list-commits', action='store_true')
+    status_parser.add_argument('-L', '--list-commits-with-hashes', action='store_true')
+    status_parser.add_argument('--no-detect-squash-merges', action='store_true')
+
+    traverse_parser = subparsers.add_parser(
+        'traverse',
+        argument_default=argparse.SUPPRESS,
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    traverse_parser.add_argument('-F', '--fetch', action='store_true')
+    traverse_parser.add_argument('-l', '--list-commits', action='store_true')
+    traverse_parser.add_argument('-M', '--merge', action='store_true')
+    traverse_parser.add_argument('-n', action='store_true')
+    traverse_parser.add_argument('--no-edit-merge', action='store_true')
+    traverse_parser.add_argument('--no-interactive-rebase', action='store_true')
+    traverse_parser.add_argument('--no-detect-squash-merges', action='store_true')
+    traverse_parser.add_argument('--push', action='store_true')
+    traverse_parser.add_argument('--no-push', action='store_true')
+    traverse_parser.add_argument('--push-untracked', action='store_true')
+    traverse_parser.add_argument('--no-push-untracked', action='store_true')
+    traverse_parser.add_argument('--return-to')
+    traverse_parser.add_argument('--start_from')
+    traverse_parser.add_argument('-w', '--whole', action='store_true')
+    traverse_parser.add_argument('-W', action='store_true')
+    traverse_parser.add_argument('-y', '--yes', action='store_true')
+
+    update_parser = subparsers.add_parser(
+        'update',
+        argument_default=argparse.SUPPRESS,
+        usage=argparse.SUPPRESS,
+        add_help=False,
+        parents=[common_args_parser])
+    update_parser.add_argument('-f', '--fork-point')
+    update_parser.add_argument('-M', '--merge', action='store_true')
+    update_parser.add_argument('-n', action='store_true')
+    update_parser.add_argument('--no-edit-merge', action='store_true')
+    update_parser.add_argument('--no-interactive-rebase', action='store_true')
+
+    subparsers.add_parser(
+        'version', usage=argparse.SUPPRESS, add_help=False, parents=[common_args_parser])
+
+    return cli_parser
+
+
+def update_cli_opts_using_parsed_args(
+        cli_opts: git_machete.options.CommandLineOptions,
+        parsed_args: argparse.Namespace) -> None:
+    for opt, arg in vars(parsed_args).items():
+        if opt == "branch":
+            cli_opts.opt_branch = arg
+        elif opt == "checked_out_since":
+            cli_opts.opt_checked_out_since = arg
+        elif opt == "color":
+            cli_opts.opt_color = arg
+        elif opt == "down_fork_point":
+            cli_opts.opt_down_fork_point = arg
+        elif opt == "debug":
+            cli_opts.opt_debug = True
+        elif opt == "draft":
+            cli_opts.opt_draft = True
+        elif opt == "fetch":
+            cli_opts.opt_fetch = True
+        elif opt == "fork_point":
+            cli_opts.opt_fork_point = arg
+        elif opt == "inferred":
+            cli_opts.opt_inferred = True
+        elif opt == "list_commits_with_hashes":
+            cli_opts.opt_list_commits = cli_opts.opt_list_commits_with_hashes = True
+        elif opt == "list_commits":
+            cli_opts.opt_list_commits = True
+        elif opt == "merge":
+            cli_opts.opt_merge = True
+        elif opt == "n":
+            cli_opts.opt_n = True
+        elif opt == "no_detect_squash_merges":
+            cli_opts.opt_no_detect_squash_merges = True
+        elif opt == "no_edit_merge":
+            cli_opts.opt_no_edit_merge = True
+        elif opt == "no_interactive_rebase":
+            cli_opts.opt_no_interactive_rebase = True
+        elif opt == "no_push":
+            cli_opts.opt_push_tracked = False
+            cli_opts.opt_push_untracked = False
+        elif opt == "no_push_untracked":
+            cli_opts.opt_push_untracked = False
+        elif opt == "onto":
+            cli_opts.opt_onto = arg
+        elif opt == "override_to":
+            cli_opts.opt_override_to = arg
+        elif opt == "override_to_inferred":
+            cli_opts.opt_override_to_inferred = True
+        elif opt == "override_to_parent":
+            cli_opts.opt_override_to_parent = True
+        elif opt == "push":
+            cli_opts.opt_push_tracked = True
+            cli_opts.opt_push_untracked = True
+        elif opt == "push_untracked":
+            cli_opts.opt_push_untracked = True
+        elif opt == "as_root":
+            cli_opts.opt_as_root = True
+        elif opt == "roots":
+            cli_opts.opt_roots = arg.split(",")
+        elif opt == "return_to":
+            cli_opts.opt_return_to = arg
+        elif opt == "stat":
+            cli_opts.opt_stat = True
+        elif opt == "start_from":
+            cli_opts.opt_start_from = arg
+        elif opt == "sync_github_prs":
+            cli_opts.opt_sync_github_prs = True
+        elif opt == "unset_override":
+            cli_opts.opt_unset_override = True
+        elif opt == "verbose":
+            cli_opts.opt_verbose = True
+        elif opt == "W":
+            cli_opts.opt_fetch = True
+            cli_opts.opt_start_from = "first-root"
+            cli_opts.opt_n = True
+            cli_opts.opt_return_to = "nearest-remaining"
+        elif opt == "whole":
+            cli_opts.opt_start_from = "first-root"
+            cli_opts.opt_n = True
+            cli_opts.opt_return_to = "nearest-remaining"
+        elif opt == "yes":
+            cli_opts.opt_yes = cli_opts.opt_no_interactive_rebase = True
+
+    if cli_opts.opt_n and cli_opts.opt_merge:
+        cli_opts.opt_no_edit_merge = True
+    elif cli_opts.opt_n and not cli_opts.opt_merge:
+        cli_opts.opt_no_interactive_rebase = True
+
+
+def set_utils_global_variables(
+        cli_opts: git_machete.options.CommandLineOptions) -> None:
+    if cli_opts.opt_color:
+        utils.ascii_only = (
+            cli_opts.opt_color == "never" or (
+                cli_opts.opt_color == "auto" and
+                not sys.stdout.isatty())
+        )
+    utils.debug_mode = cli_opts.opt_debug
+    utils.verbose_mode = cli_opts.opt_verbose
+
+
+def validate_python_version() -> None:
+    if sys.version_info[:2] < (3, 6):
         version_str = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
         sys.stderr.write(
-            f"Python {version_str} is no longer supported. Please switch to Python 3.6 or higher.\n")
+            f"Python {version_str} is no longer supported. "
+            "Please switch to Python 3.6 or higher.\n")
         sys.exit(1)
 
-    def parse_options(in_args: List[str], short_opts: str = "", long_opts: List[str] = [],
-                      allow_intermixing_options_and_params: bool = True) -> List[str]:
-
-        fun = getopt.gnu_getopt if allow_intermixing_options_and_params else getopt.getopt
-        opts, rest = fun(in_args, short_opts + "hv",
-                         long_opts + ['debug', 'help', 'verbose', 'version'])
-
-        for opt, arg in opts:
-            if opt in ("-b", "--branch"):
-                cli_opts.opt_branch = arg
-            elif opt in ("-C", "--checked-out-since"):
-                cli_opts.opt_checked_out_since = arg
-            elif opt == "--color":
-                cli_opts.opt_color = arg
-            elif opt in ("-d", "--down-fork-point"):
-                cli_opts.opt_down_fork_point = arg
-            elif opt == "--debug":
-                git_machete.options.CommandLineOptions.opt_debug = True
-            elif opt == "--draft":
-                cli_opts.opt_draft = True
-            elif opt in ("-F", "--fetch"):
-                cli_opts.opt_fetch = True
-            elif opt in ("-f", "--fork-point"):
-                cli_opts.opt_fork_point = arg
-            elif opt in ("-H", "--sync-github-prs"):
-                cli_opts.opt_sync_github_prs = True
-            elif opt in ("-h", "--help"):
-                usage(cmd)
-                sys.exit()
-            elif opt == "--inferred":
-                cli_opts.opt_inferred = True
-            elif opt in ("-L", "--list-commits-with-hashes"):
-                cli_opts.opt_list_commits = cli_opts.opt_list_commits_with_hashes = True
-            elif opt in ("-l", "--list-commits"):
-                cli_opts.opt_list_commits = True
-            elif opt in ("-M", "--merge"):
-                cli_opts.opt_merge = True
-            elif opt == "-n":
-                cli_opts.opt_n = True
-            elif opt == "--no-detect-squash-merges":
-                cli_opts.opt_no_detect_squash_merges = True
-            elif opt == "--no-edit-merge":
-                cli_opts.opt_no_edit_merge = True
-            elif opt == "--no-interactive-rebase":
-                cli_opts.opt_no_interactive_rebase = True
-            elif opt == "--no-push":
-                cli_opts.opt_push_tracked = False
-                cli_opts.opt_push_untracked = False
-            elif opt == "--no-push-untracked":
-                cli_opts.opt_push_untracked = False
-            elif opt in ("-o", "--onto"):
-                cli_opts.opt_onto = arg
-            elif opt == "--override-to":
-                cli_opts.opt_override_to = arg
-            elif opt == "--override-to-inferred":
-                cli_opts.opt_override_to_inferred = True
-            elif opt == "--override-to-parent":
-                cli_opts.opt_override_to_parent = True
-            elif opt == "--push":
-                cli_opts.opt_push_tracked = True
-                cli_opts.opt_push_untracked = True
-            elif opt == "--push-untracked":
-                cli_opts.opt_push_untracked = True
-            elif opt in ("-R", "--as-root"):
-                cli_opts.opt_as_root = True
-            elif opt in ("-r", "--roots"):
-                cli_opts.opt_roots = arg.split(",")
-            elif opt == "--return-to":
-                cli_opts.opt_return_to = arg
-            elif opt in ("-s", "--stat"):
-                cli_opts.opt_stat = True
-            elif opt == "--start-from":
-                cli_opts.opt_start_from = arg
-            elif opt == "--unset-override":
-                cli_opts.opt_unset_override = True
-            elif opt in ("-v", "--verbose"):
-                git_machete.options.CommandLineOptions.opt_verbose = True
-            elif opt == "--version":
-                version()
-                sys.exit()
-            elif opt == "-W":
-                cli_opts.opt_fetch = True
-                cli_opts.opt_start_from = "first-root"
-                cli_opts.opt_n = True
-                cli_opts.opt_return_to = "nearest-remaining"
-            elif opt in ("-w", "--whole"):
-                cli_opts.opt_start_from = "first-root"
-                cli_opts.opt_n = True
-                cli_opts.opt_return_to = "nearest-remaining"
-            elif opt in ("-y", "--yes"):
-                cli_opts.opt_yes = cli_opts.opt_no_interactive_rebase = True
-
-        if cli_opts.opt_color not in ("always", "auto", "never"):
-            raise MacheteException(
-                "Invalid argument for `--color`. Valid arguments: `always|auto|never`.")
-        else:
-            utils.ascii_only = (
-                cli_opts.opt_color == "never" or (
-                    cli_opts.opt_color == "auto" and not sys.stdout.isatty())
-            )
 
-        if cli_opts.opt_as_root and cli_opts.opt_onto:
-            raise MacheteException(
-                "Option `-R/--as-root` cannot be specified together with `-o/--onto`.")
-
-        if cli_opts.opt_no_edit_merge and not cli_opts.opt_merge:
-            raise MacheteException(
-                "Option `--no-edit-merge` only makes sense when using merge and must be specified together with `-M/--merge`.")
-        if cli_opts.opt_no_interactive_rebase and cli_opts.opt_merge:
-            raise MacheteException(
-                "Option `--no-interactive-rebase` only makes sense when using rebase and cannot be specified together with `-M/--merge`.")
-        if cli_opts.opt_down_fork_point and cli_opts.opt_merge:
-            raise MacheteException(
-                "Option `-d/--down-fork-point` only makes sense when using rebase and cannot be specified together with `-M/--merge`.")
-        if cli_opts.opt_fork_point and cli_opts.opt_merge:
-            raise MacheteException(
-                "Option `-f/--fork-point` only makes sense when using rebase and cannot be specified together with `-M/--merge`.")
-
-        if cli_opts.opt_n and cli_opts.opt_merge:
-            cli_opts.opt_no_edit_merge = True
-        if cli_opts.opt_n and not cli_opts.opt_merge:
-            cli_opts.opt_no_interactive_rebase = True
+def get_branch_arg_or_current_branch(
+        cli_opts: git_machete.options.CommandLineOptions, git_context: GitContext) -> str:
+    return cli_opts.opt_branch or git_context.get_current_branch()
 
-        return rest
-
-    def expect_no_param(in_args: List[str], extra_explanation: str = '') -> None:
-        if len(in_args) > 0:
-            raise MacheteException(f"No argument expected for `{cmd}`{extra_explanation}")
-
-    def check_optional_param(in_args: List[str]) -> Optional[str]:
-        if not in_args:
-            return None
-        elif len(in_args) > 1:
-            raise MacheteException(f"`{cmd}` accepts at most one argument")
-        elif not in_args[0]:
-            raise MacheteException(f"Argument to `{cmd}` cannot be empty")
-        elif in_args[0][0] == "-":
-            raise MacheteException(f"Option `{in_args[0]}` not recognized")
-        else:
-            return in_args[0]
-
-    def check_required_param(in_args: List[str], allowed_values_string: str) -> str:
-        if not in_args or len(in_args) > 1:
-            raise MacheteException(
-                f"`{cmd}` expects exactly one argument: one of {allowed_values_string}")
-        elif not in_args[0]:
-            raise MacheteException(
-                f"Argument to `{cmd}` cannot be empty; expected one of {allowed_values_string}")
-        elif in_args[0][0] == "-":
-            raise MacheteException(f"Option `{in_args[0]}` not recognized")
-        else:
-            return in_args[0]
+
+def launch(orig_args: List[str]) -> None:
+    if not orig_args:
+        print(get_help_description())
+        sys.exit(2)
+
+    cli_opts = git_machete.options.CommandLineOptions()
+    git = GitContext(cli_opts)
 
     try:
         machete_client = MacheteClient(cli_opts, git)
-        cmd = None
-        # Let's first extract the common options like `--help` or `--verbose` that might appear BEFORE the command,
-        # as in e.g. `git machete --verbose status`.
-        cmd_and_args = parse_options(orig_args, allow_intermixing_options_and_params=False)
-        # Subsequent calls to `parse_options` will, in turn, extract the options appearing AFTER the command.
-        if not cmd_and_args:
-            usage()
-            sys.exit(2)
-        cmd = cmd_and_args[0]
-        args = cmd_and_args[1:]
-
-        if cmd != "help":
-            if cmd != "discover":
-                if not os.path.exists(machete_client.definition_file_path):
-                    # We're opening in "append" and not "write" mode to avoid a race condition:
-                    # if other process writes to the file between we check the result of `os.path.exists` and call `open`,
-                    # then open(..., "w") would result in us clearing up the file contents, while open(..., "a") has no effect.
-                    with open(machete_client.definition_file_path, "a"):
-                        pass
-                elif os.path.isdir(machete_client.definition_file_path):
-                    # Extremely unlikely case, basically checking if anybody tampered with the repository.
-                    raise MacheteException(
-                        f"{machete_client.definition_file_path} is a directory rather than a regular file, aborting")
+
+        cli_parser: argparse.ArgumentParser = create_cli_parser()
+        parsed_cli: argparse.Namespace = cli_parser.parse_args(orig_args)
+        parsed_cli_as_dict: Dict[str, str] = vars(parsed_cli)
+
+        update_cli_opts_using_parsed_args(cli_opts, parsed_cli)
+        cli_opts.validate()
+        set_utils_global_variables(cli_opts)
+
+        cmd = parsed_cli.command
+
+        if cmd not in {'help', 'discover'}:
+            if not os.path.exists(machete_client.definition_file_path):
+                # We're opening in "append" and not "write" mode to avoid a race condition:
+                # if other process writes to the file between we check the
+                # result of `os.path.exists` and call `open`,
+                # then open(..., "w") would result in us clearing up the file
+                # contents, while open(..., "a") has no effect.
+                with open(machete_client.definition_file_path, "a"):
+                    pass
+            elif os.path.isdir(machete_client.definition_file_path):
+                # Extremely unlikely case, basically checking if anybody
+                # tampered with the repository.
+                raise MacheteException(
+                    f"{machete_client.definition_file_path} is a directory "
+                    "rather than a regular file, aborting")
 
         if cmd == "add":
-            param = check_optional_param(parse_options(args, "o:Ry", ["onto=", "as-root", "yes"]))
             machete_client.read_definition_file()
-            machete_client.add(param or git.get_current_branch())
+            branch = get_branch_arg_or_current_branch(cli_opts, git)
+            machete_client.add(branch)
         elif cmd == "advance":
-            args1 = parse_options(args, "y", ["yes"])
-            expect_no_param(args1)
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
             machete_client.expect_in_managed_branches(current_branch)
             machete_client.advance(current_branch)
         elif cmd == "anno":
-            params = parse_options(args, "b:H", ["branch=", "sync-github-prs"])
             machete_client.read_definition_file(verify_branches=False)
             if cli_opts.opt_sync_github_prs:
                 machete_client.sync_annotations_to_github_prs()
             else:
                 branch = cli_opts.opt_branch or git.get_current_branch()
                 machete_client.expect_in_managed_branches(branch)
-                if params:
-                    machete_client.annotate(branch, params)
+                if parsed_cli.annotation_text:
+                    machete_client.annotate(branch, parsed_cli.annotation_text)
                 else:
                     machete_client.print_annotation(branch)
         elif cmd == "delete-unmanaged":
-            expect_no_param(parse_options(args, "y", ["yes"]))
             machete_client.read_definition_file()
             machete_client.delete_unmanaged()
-        elif cmd in ("d", "diff"):
-            param = check_optional_param(parse_options(args, "s", ["stat"]))
+        elif cmd in {"diff", alias_by_command["diff"]}:
             machete_client.read_definition_file()
-            machete_client.diff(param)  # passing None if not specified
+            machete_client.diff(parsed_cli.branch)  # passing None if not specified
         elif cmd == "discover":
-            expect_no_param(parse_options(args, "C:lr:y",
-                                          ["checked-out-since=", "list-commits", "roots=", "yes"]))
             # No need to read definition file.
             machete_client.discover_tree()
-        elif cmd in ("e", "edit"):
-            expect_no_param(parse_options(args))
+        elif cmd in {"edit", alias_by_command["edit"]}:
             # No need to read definition file.
             machete_client.edit()
         elif cmd == "file":
-            expect_no_param(parse_options(args))
             # No need to read definition file.
             print(os.path.abspath(machete_client.definition_file_path))
         elif cmd == "fork-point":
-            long_options = ["inferred", "override-to=", "override-to-inferred",
-                            "override-to-parent", "unset-override"]
-            param = check_optional_param(parse_options(args, "", long_options))
             machete_client.read_definition_file()
-            branch = param or git.get_current_branch()
-            if len(list(filter(None, [cli_opts.opt_inferred, cli_opts.opt_override_to,
-                                      cli_opts.opt_override_to_inferred,
-                                      cli_opts.opt_override_to_parent,
-                                      cli_opts.opt_unset_override]))) > 1:
-                long_options_string = ", ".join(map(lambda x: x.replace("=", ""), long_options))
-                raise MacheteException(
-                    f"At most one of {long_options_string} options may be present")
+            branch = get_branch_arg_or_current_branch(cli_opts, git)
             if cli_opts.opt_inferred:
                 print(machete_client.fork_point(branch, use_overrides=False))
             elif cli_opts.opt_override_to:
                 machete_client.set_fork_point_override(branch, cli_opts.opt_override_to)
             elif cli_opts.opt_override_to_inferred:
-                machete_client.set_fork_point_override(branch, machete_client.fork_point(branch,
-                                                                                         use_overrides=False))
+                machete_client.set_fork_point_override(
+                    branch, machete_client.fork_point(branch, use_overrides=False))
             elif cli_opts.opt_override_to_parent:
                 upstream = machete_client.up_branch.get(branch)
                 if upstream:
@@ -354,192 +562,157 @@ def launch(orig_args: List[str]) -> None:
                 machete_client.unset_fork_point_override(branch)
             else:
                 print(machete_client.fork_point(branch, use_overrides=True))
-        elif cmd in ("g", "go"):
-            param = check_required_param(parse_options(args),
-                                         allowed_directions(allow_current=False))
+        elif cmd in {"go", alias_by_command["go"]}:
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
-            dest = machete_client.parse_direction(param, current_branch, allow_current=False,
-                                                  down_pick_mode=True)
+            dest = machete_client.parse_direction(
+                parsed_cli.direction, current_branch, allow_current=False, down_pick_mode=True)
             if dest != current_branch:
                 git.checkout(dest)
         elif cmd == "github":
-            github_allowed_subcommands = "anno-prs|checkout-prs|create-pr|retarget-pr"
-            list_args = parse_options(args) if args[0] == 'checkout-prs' \
-                else check_required_param(parse_options(args, "", ["draft"]), github_allowed_subcommands)
-            if not list_args:
-                raise MacheteException(f"`git machete github` expects argument(s): {github_allowed_subcommands}")
-
-            param = args[0]
+            github_subcommand = parsed_cli.subcommand
             machete_client.read_definition_file()
-            if param == "anno-prs":
+
+            if 'draft' in parsed_cli and github_subcommand not in {'create-pr'}:
+                raise MacheteException(
+                    "'--draft' option is only valid with 'create-pr' subcommand.")
+            if 'pr_no' in parsed_cli and github_subcommand not in {'checkout-prs'}:
+                raise MacheteException(
+                    "'pr_no' argument is only valid with 'checkout-prs' subcommand.")
+
+            if github_subcommand == "anno-prs":
                 machete_client.sync_annotations_to_github_prs()
-            elif param == "checkout-prs":
-                if len(list_args) == 1:
+            elif github_subcommand == "checkout-prs":
+                if 'pr_no' not in parsed_cli:
                     raise MacheteException(
-                        "Argument to `git machete github checkout-prs` cannot be empty; expected PR number.")
+                        "Argument to `git machete github checkout-prs` cannot be"
+                        " empty; expected PR number.")
                 try:
-                    pr_no: int = int(list_args[1])
+                    pr_no: int = int(parsed_cli.pr_no)
                 except ValueError:
                     raise MacheteException("PR number is not integer value!")
                 machete_client.checkout_github_prs(pr_no)
-            elif param == "create-pr":
-                check_required_param(parse_options(args, "", ["draft"]), github_allowed_subcommands)
+            elif github_subcommand == "create-pr":
                 current_branch = git.get_current_branch()
                 machete_client.create_github_pr(current_branch, draft=cli_opts.opt_draft)
-            elif param == "retarget-pr":
+            elif github_subcommand == "retarget-pr":
                 current_branch = git.get_current_branch()
                 machete_client.expect_in_managed_branches(current_branch)
                 machete_client.retarget_github_pr(current_branch)
-            else:
-                raise MacheteException(
-                    f"`github` requires a subcommand: one of `{github_allowed_subcommands}`")
         elif cmd == "help":
-            param = check_optional_param(parse_options(args))
             # No need to read definition file.
-            usage(param)
+            print(get_help_description(parsed_cli.topic_or_cmd))
         elif cmd == "is-managed":
-            param = check_optional_param(parse_options(args))
             machete_client.read_definition_file()
-            branch = param or git.get_current_branch_or_none()
+            branch = get_branch_arg_or_current_branch(cli_opts, git)
             if branch is None or branch not in machete_client.managed_branches:
                 sys.exit(1)
         elif cmd == "list":
-            list_allowed_values = "addable|managed|slidable|slidable-after <branch>|unmanaged|with-overridden-fork-point"
-            list_args = parse_options(args)
-            if not list_args:
-                raise MacheteException(
-                    f"`git machete list` expects argument(s): {list_allowed_values}")
-            elif not list_args[0]:
+            category = parsed_cli.category
+            if category == 'slidable-after' and 'branch' not in parsed_cli_as_dict:
                 raise MacheteException(
-                    f"Argument to `git machete list` cannot be empty; expected {list_allowed_values}")
-            elif list_args[0][0] == "-":
-                raise MacheteException(f"Option `{list_args[0]}` not recognized")
-            elif list_args[0] not in (
-                    "addable", "managed", "slidable", "slidable-after", "unmanaged",
-                    "with-overridden-fork-point"):
-                raise MacheteException(f"Usage: git machete list {list_allowed_values}")
-            elif len(list_args) > 2:
-                raise MacheteException(f"Too many arguments to `git machete list {list_args[0]}` ")
-            elif (list_args[0] in (
-                    "addable", "managed", "slidable", "unmanaged", "with-overridden-fork-point") and
-                  len(list_args) > 1):
+                    f"`git machete list {category}` requires an extra <branch> argument")
+            elif category != 'slidable-after' and 'branch' in parsed_cli_as_dict:
                 raise MacheteException(
-                    f"`git machete list {list_args[0]}` does not expect extra arguments")
-            elif list_args[0] == "slidable-after" and len(list_args) != 2:
-                raise MacheteException(
-                    f"`git machete list {list_args[0]}` requires an extra <branch> argument")
+                    f"`git machete list {category}` does not expect extra arguments")
 
-            param = list_args[0]
             machete_client.read_definition_file()
             res = []
-            if param == "addable":
+            if category == "addable":
                 def strip_first_fragment(remote_branch: str) -> str:
                     return re.sub("^[^/]+/", "", remote_branch)
 
                 remote_counterparts_of_local_branches = utils.map_truthy_only(
-                    lambda branch: git.get_combined_counterpart_for_fetching_of_branch(branch),
+                    lambda _branch: git.get_combined_counterpart_for_fetching_of_branch(_branch),
                     git.get_local_branches())
                 qualifying_remote_branches = excluding(git.get_remote_branches(),
                                                        remote_counterparts_of_local_branches)
                 res = excluding(git.get_local_branches(), machete_client.managed_branches) + list(
                     map(strip_first_fragment, qualifying_remote_branches))
-            elif param == "managed":
+            elif category == "managed":
                 res = machete_client.managed_branches
-            elif param == "slidable":
+            elif category == "slidable":
                 res = machete_client.slidable()
-            elif param == "slidable-after":
-                b_arg = list_args[1]
-                machete_client.expect_in_managed_branches(b_arg)
-                res = machete_client.slidable_after(b_arg)
-            elif param == "unmanaged":
+            elif category == "slidable-after":
+                machete_client.expect_in_managed_branches(parsed_cli.branch)
+                res = machete_client.slidable_after(parsed_cli.branch)
+            elif category == "unmanaged":
                 res = excluding(git.get_local_branches(), machete_client.managed_branches)
-            elif param == "with-overridden-fork-point":
+            elif category == "with-overridden-fork-point":
                 res = list(
-                    filter(lambda branch: machete_client.has_any_fork_point_override_config(branch),
-                           git.get_local_branches()))
+                    filter(
+                        lambda _branch: machete_client.has_any_fork_point_override_config(_branch),
+                        git.get_local_branches()))
 
             if res:
                 print("\n".join(res))
-        elif cmd in ("l", "log"):
-            param = check_optional_param(parse_options(args))
+        elif cmd in {"log", alias_by_command["log"]}:
             machete_client.read_definition_file()
-            machete_client.log(param or git.get_current_branch())
+            branch = get_branch_arg_or_current_branch(cli_opts, git)
+            machete_client.log(branch)
         elif cmd == "reapply":
-            args1 = parse_options(args, "f:", ["fork-point="])
-            expect_no_param(args1, ". Use `-f` or `--fork-point` to specify the fork point commit")
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
-            git.rebase_onto_ancestor_commit(current_branch,
-                                            cli_opts.opt_fork_point or machete_client.fork_point(
-                                                current_branch, use_overrides=True))
+            git.rebase_onto_ancestor_commit(
+                current_branch,
+                cli_opts.opt_fork_point or machete_client.fork_point(
+                    current_branch, use_overrides=True))
         elif cmd == "show":
-            args1 = parse_options(args)
-            param = check_required_param(args1[:1], allowed_directions(allow_current=True))
-            branch = check_optional_param(args1[1:])
-            if param == "current" and branch is not None:
+            direction = parsed_cli.direction
+            if direction == "current" and 'branch' in parsed_cli:
                 raise MacheteException(
-                    f'`show current` with a branch (`{branch}`) does not make sense')
+                    '`show current` with a <branch> argument does not make sense')
             machete_client.read_definition_file(verify_branches=False)
-            print(machete_client.parse_direction(param, branch or git.get_current_branch(),
-                                                 allow_current=True, down_pick_mode=False))
+            branch = get_branch_arg_or_current_branch(cli_opts, git)
+            print(
+                machete_client.parse_direction(
+                    direction,
+                    branch,
+                    allow_current=True,
+                    down_pick_mode=False
+                )
+            )
         elif cmd == "slide-out":
-            params = parse_options(args, "d:Mn", ["down-fork-point=", "merge", "no-edit-merge",
-                                                  "no-interactive-rebase"])
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
-            machete_client.slide_out(params or [git.get_current_branch()])
+            branches = parsed_cli_as_dict.get('branches', [git.get_current_branch()])
+            machete_client.slide_out(list(branches))
         elif cmd == "squash":
-            args1 = parse_options(args, "f:", ["fork-point="])
-            expect_no_param(args1, ". Use `-f` or `--fork-point` to specify the fork point commit")
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
-            machete_client.squash(current_branch,
-                                  cli_opts.opt_fork_point or machete_client.fork_point(
-                                      current_branch, use_overrides=True))
-        elif cmd in ("s", "status"):
-            expect_no_param(parse_options(args, "Ll",
-                                          ["color=", "list-commits-with-hashes", "list-commits",
-                                           "no-detect-squash-merges"]))
+            machete_client.squash(
+                current_branch,
+                cli_opts.opt_fork_point or machete_client.fork_point(
+                    current_branch, use_overrides=True))
+        elif cmd in {"status", alias_by_command["status"]}:
             machete_client.read_definition_file()
             machete_client.expect_at_least_one_managed_branch()
             machete_client.status(warn_on_yellow_edges=True)
         elif cmd == "traverse":
-            traverse_long_opts = ["fetch", "list-commits", "merge",
-                                  "no-detect-squash-merges",
-                                  "no-edit-merge", "no-interactive-rebase",
-                                  "no-push", "no-push-untracked", "push", "push-untracked",
-                                  "return-to=", "start-from=", "whole", "yes"]
-            expect_no_param(parse_options(args, "FlMnWwy", traverse_long_opts))
-            if cli_opts.opt_start_from not in ("here", "root", "first-root"):
+            if cli_opts.opt_start_from not in {"here", "root", "first-root"}:
                 raise MacheteException(
-                    "Invalid argument for `--start-from`. Valid arguments: `here|root|first-root`.")
+                    "Invalid argument for `--start-from`. "
+                    "Valid arguments: `here|root|first-root`.")
             if cli_opts.opt_return_to not in ("here", "nearest-remaining", "stay"):
                 raise MacheteException(
-                    "Invalid argument for `--return-to`. Valid arguments: here|nearest-remaining|stay.")
+                    "Invalid argument for `--return-to`. "
+                    "Valid arguments: `here|nearest-remaining|stay`.")
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
             machete_client.traverse()
         elif cmd == "update":
-            args1 = parse_options(args, "f:Mn", ["fork-point=", "merge", "no-edit-merge",
-                                                 "no-interactive-rebase"])
-            expect_no_param(args1, ". Use `-f` or `--fork-point` to specify the fork point commit")
             machete_client.read_definition_file()
             git.expect_no_operation_in_progress()
             machete_client.update()
         elif cmd == "version":
             version()
             sys.exit()
-        else:
-            short_usage()
-            raise MacheteException(
-                f"\nUnknown command: `{cmd}`. Use `git machete help` to list possible commands")
 
-    except getopt.GetoptError as e:
-        short_usage()
+    except (argparse.ArgumentError, argparse.ArgumentTypeError) as e:
+        print(get_short_general_usage())
         sys.stderr.write(f"\n{e}\n")
         sys.exit(2)
     except MacheteException as e:
@@ -554,10 +727,16 @@ def launch(orig_args: List[str]) -> None:
         if initial_current_directory and not utils.does_directory_exist(initial_current_directory):
             nearest_existing_parent_directory = initial_current_directory
             while not utils.does_directory_exist(nearest_existing_parent_directory):
-                nearest_existing_parent_directory = os.path.join(nearest_existing_parent_directory,
-                                                                 os.path.pardir)
+                nearest_existing_parent_directory = os.path.join(
+                    nearest_existing_parent_directory, os.path.pardir)
             warn(f"current directory {initial_current_directory} no longer exists, "
-                 f"the nearest existing parent directory is {os.path.abspath(nearest_existing_parent_directory)}")
+                 "the nearest existing parent directory is "
+                 f"{os.path.abspath(nearest_existing_parent_directory)}")
+
+
+def main() -> None:
+    validate_python_version()
+    launch(sys.argv[1:])
 
 
 if __name__ == "__main__":
diff --git a/git_machete/options.py b/git_machete/options.py
index 946a8b2..90540ed 100644
--- a/git_machete/options.py
+++ b/git_machete/options.py
@@ -1,15 +1,16 @@
 from typing import Optional, List
 
+from git_machete.exceptions import MacheteException
+
 
 class CommandLineOptions:
-    opt_debug: bool = False
-    opt_verbose: bool = False
 
     def __init__(self) -> None:
         self.opt_as_root: bool = False
         self.opt_branch: Optional[str] = None
         self.opt_checked_out_since: Optional[str] = None
         self.opt_color: str = "auto"
+        self.opt_debug: bool = False
         self.opt_down_fork_point: Optional[str] = None
         self.opt_draft: bool = False
         self.opt_fetch: bool = False
@@ -34,4 +35,26 @@ class CommandLineOptions:
         self.opt_stat: bool = False
         self.opt_sync_github_prs: bool = False
         self.opt_unset_override: bool = False
+        self.opt_verbose: bool = False
         self.opt_yes: bool = False
+
+    def validate(self) -> None:
+        if self.opt_as_root and self.opt_onto:
+            raise MacheteException(
+                "Option `-R/--as-root` cannot be specified together with `-o/--onto`.")
+        if self.opt_no_edit_merge and not self.opt_merge:
+            raise MacheteException(
+                "Option `--no-edit-merge` only makes sense when using merge and "
+                "must be specified together with `-M/--merge`.")
+        if self.opt_no_interactive_rebase and self.opt_merge:
+            raise MacheteException(
+                "Option `--no-interactive-rebase` only makes sense when using "
+                "rebase and cannot be specified together with `-M/--merge`.")
+        if self.opt_down_fork_point and self.opt_merge:
+            raise MacheteException(
+                "Option `-d/--down-fork-point` only makes sense when using "
+                "rebase and cannot be specified together with `-M/--merge`.")
+        if self.opt_fork_point and self.opt_merge:
+            raise MacheteException(
+                "Option `-f/--fork-point` only makes sense when using rebase and"
+                " cannot be specified together with `-M/--merge`.")
diff --git a/git_machete/utils.py b/git_machete/utils.py
index 4672e73..1892fae 100644
--- a/git_machete/utils.py
+++ b/git_machete/utils.py
@@ -6,7 +6,6 @@ import sys
 import re
 import subprocess
 
-from git_machete.options import CommandLineOptions
 from git_machete.constants import EscapeCodes
 
 T = TypeVar('T')
@@ -18,6 +17,8 @@ displayed_warnings: Set[str] = set()
 current_directory_confirmed_to_exist: bool = False
 
 ascii_only: bool = False
+debug_mode: bool = False
+verbose_mode: bool = False
 
 
 def excluding(iterable: Iterable[T], s: Iterable[T]) -> List[T]:
@@ -154,7 +155,7 @@ def get_pretty_choices(*choices: str) -> str:
 
 
 def debug(hdr: str, msg: str) -> None:
-    if CommandLineOptions.opt_debug:
+    if debug_mode:
         sys.stderr.write(f"{bold(hdr)}: {dim(msg)}\n")
 
 
@@ -162,9 +163,9 @@ def run_cmd(cmd: str, *args: str, **kwargs: Any) -> int:
     chdir_upwards_until_current_directory_exists()
 
     flat_cmd: str = get_cmd_shell_repr(cmd, *args, **kwargs)
-    if CommandLineOptions.opt_debug:
+    if debug_mode:
         sys.stderr.write(bold(f">>> {flat_cmd}") + "\n")
-    elif CommandLineOptions.opt_verbose:
+    elif verbose_mode:
         sys.stderr.write(flat_cmd + "\n")
 
     exit_code: int = subprocess.call([cmd] + list(args), **kwargs)
@@ -174,7 +175,7 @@ def run_cmd(cmd: str, *args: str, **kwargs: Any) -> int:
     # In practice, it's mostly 'git checkout' that carries such risk.
     mark_current_directory_as_possibly_non_existent()
 
-    if CommandLineOptions.opt_debug and exit_code != 0:
+    if debug_mode and exit_code != 0:
         sys.stderr.write(dim(f"<exit code: {exit_code}>\n\n"))
     return exit_code
 
@@ -203,9 +204,9 @@ def popen_cmd(cmd: str, *args: str, **kwargs: Any) -> Tuple[int, str, str]:
     chdir_upwards_until_current_directory_exists()
 
     flat_cmd = get_cmd_shell_repr(cmd, *args, **kwargs)
-    if CommandLineOptions.opt_debug:
+    if debug_mode:
         sys.stderr.write(bold(f">>> {flat_cmd}") + "\n")
-    elif CommandLineOptions.opt_verbose:
+    elif verbose_mode:
         sys.stderr.write(flat_cmd + "\n")
 
     process = subprocess.Popen([cmd] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
@@ -214,7 +215,7 @@ def popen_cmd(cmd: str, *args: str, **kwargs: Any) -> Tuple[int, str, str]:
     stderr: str = stderr_bytes.decode('utf-8')
     exit_code: int = process.returncode
 
-    if CommandLineOptions.opt_debug:
+    if debug_mode:
         if exit_code != 0:
             sys.stderr.write(colored(f"<exit code: {exit_code}>\n\n", EscapeCodes.RED))
         if stdout:


Test patch:
diff --git a/git_machete/tests/functional/test_machete.py b/git_machete/tests/functional/test_machete.py
index 708486d..e60bd26 100644
--- a/git_machete/tests/functional/test_machete.py
+++ b/git_machete/tests/functional/test_machete.py
@@ -883,11 +883,11 @@ class MacheteTester(unittest.TestCase):
             for command in commands:
                 self.launch_command("help", command)
 
-                if command not in ("format", "hooks"):
+                if command not in {"format", "hooks"}:
                     try:
                         self.launch_command(command, "--help")
                     except SystemExit as e:
-                        self.assertIs(e.code, None)
+                        self.assertIs(e.code, 0)
                     except Exception as e:
                         self.fail(f'Unexpected exception raised: {e}')
                     else: