Repository: VirtusLab/git-machete
Pull request #720 (2022-10-31 14:05:07)

Release v3.13.0


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index c59bcff..ab62214 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -98,6 +98,16 @@ jobs:
           path: docs/html/
           destination: docs
 
+  "python 3_11 git 2_38_1":
+    executor: ubuntu_executor
+    steps:
+      - checkout
+      - run:
+          name: Run tests against Python 3.11 and git 2.38.1
+          command: PYTHON_VERSION=3.11 GIT_VERSION=2.38.1 bash ci/tox/ci-run.sh
+      - store_test_results:
+          path: test-results/
+
   "dry run deploy":
     executor: ubuntu_executor
     steps:
@@ -192,6 +202,7 @@ mandatory_jobs: &mandatory_jobs
   - pep8 and python 3_8 git 2_25_0
   - coverage and python 3_9 git 2_30_1
   - docs and python 3_10 git 2_33_1
+  - python 3_11 git 2_38_1
 
 workflows:
   version: 2
@@ -203,6 +214,7 @@ workflows:
       - pep8 and python 3_8 git 2_25_0
       - coverage and python 3_9 git 2_30_1
       - docs and python 3_10 git 2_33_1
+      - python 3_11 git 2_38_1
       - dry run deploy:
           filters:
             branches:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index cebb402..fa0db0c 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,14 @@
 # Release notes
 
+## New in git-machete 3.13.0
+
+- improved: modify formatting of command line documentation
+- added: `git machete help <command>` now accepts command aliases
+- fixed: removed stack trace when terminating `git machete` command prompt with `Ctrl+D`
+- added: support for Python 3.11
+- added: machete config key `machete.traverse.push` that controls default behavior of `traverse` command
+- improved: formatting of the `config` section in the sphinx documentation
+
 ## New in git-machete 3.12.5
 
 - added: `git machete github retarget-pr` now updates annotation for the branch associated with the retargeted PR
diff --git a/ci/homebrew/ci-deploy.sh b/ci/homebrew/ci-deploy.sh
index f783f0a..274357b 100644
--- a/ci/homebrew/ci-deploy.sh
+++ b/ci/homebrew/ci-deploy.sh
@@ -30,7 +30,7 @@ else
   brew bump-formula-pr --write-only --no-browse --verbose --url "$url" --sha256 "$sha256" git-machete
 
   echo "Attempt to install the formula locally"
-  attempts=3
+  attempts=5
   i=1
   while true; do
     if brew install --build-from-source --formula /home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/git-machete.rb; then
@@ -38,7 +38,7 @@ else
     elif (( i < attempts )); then
       echo "Retrying the installation..."
       i=$((i + 1))
-      sleep 30
+      sleep 60
     else
       echo "Installing the formula locally did not succeed despite $attempts attempts"
       exit 1
diff --git a/ci/snap/ci-deploy.sh b/ci/snap/ci-deploy.sh
index ac4b79d..44e76c7 100644
--- a/ci/snap/ci-deploy.sh
+++ b/ci/snap/ci-deploy.sh
@@ -22,8 +22,7 @@ if [[ ${1-} == "--dry-run" || ${CIRCLE_BRANCH-} != "master" ]]; then
   git machete version
   sudo snap remove git-machete
 else
-  echo "$SNAPCRAFT_LOGIN_CREDENTIALS_CONTENTS_BASE64" | base64 -d > ~/.snapcraft.login
-  snapcraft login --with ~/.snapcraft.login
+  export SNAPCRAFT_STORE_CREDENTIALS=$SNAPCRAFT_LOGIN_CREDENTIALS_CONTENTS_BASE64
   snapcraft upload --release=stable git-machete*.snap
   snapcraft status git-machete
 fi
diff --git a/docs/enforce-docs-up-to-date.sh b/docs/enforce-docs-up-to-date.sh
index 4869016..a77a3de 100755
--- a/docs/enforce-docs-up-to-date.sh
+++ b/docs/enforce-docs-up-to-date.sh
@@ -2,10 +2,10 @@
 
 set -e -o pipefail -u
 
-current_docs=$(<git_machete/generated_long_docs.py)
+current_docs=$(<git_machete/generated_docs.py)
 generated_docs=$(python docs/generate_py_docs.py)
 
 if [[ "$current_docs" != "$generated_docs" ]]; then
-  echo "Command line docs are not up-to-date. Please regenerate docs via tox -e py-docs"
+  echo "Command line docs are not up-to-date. Please regenerate docs via 'tox -e py-docs'"
   exit 1
 fi
diff --git a/docs/generate_py_docs.py b/docs/generate_py_docs.py
index 884cec8..9000a8d 100644
--- a/docs/generate_py_docs.py
+++ b/docs/generate_py_docs.py
@@ -1,4 +1,4 @@
-from bs4 import BeautifulSoup
+from bs4 import BeautifulSoup, Comment
 from docutils import core
 import os
 from os.path import isfile, join
@@ -19,14 +19,17 @@ def rst2html(input_string: str, source_path: str = None, destination_path: str =
     return parts['body']
 
 
-def html2txt(html: str):
-    html_elements = BeautifulSoup(html, features="html.parser")
-    INDENT = 3 * ' '
+def html2txt(html_: str):
+    html_elements = BeautifulSoup(html_, features="html.parser")
 
     # remove style tags
     for tag in html_elements.select('style'):
         tag.decompose()
 
+    # remove comments
+    for comment in html_elements.findAll(string=lambda string: isinstance(string, Comment)):
+        comment.extract()
+
     # remove non-breaking spaces
     for tag in html_elements.select('td'):
         if tag.text == u'\xa0':
@@ -81,33 +84,33 @@ def html2txt(html: str):
                 lines = tag.text.splitlines()
                 tag.string = f' - {lines[0]}'
                 if len(lines) > 1:
-                    tag.string += indent('\n' + '\n'.join(lines[1:]), INDENT)
+                    tag.string += indent('\n' + '\n'.join(lines[1:]), INDENT_LEN_3)
 
     # add indent and bullet points to the bulleted list
     for tag in html_elements.select('li'):
         if tag.parent.parent is not None:
             if tag.parent.parent.name != 'li':
                 lines = tag.text.splitlines()
-                tag.string = f'\n{INDENT}* {lines[0]}'
+                tag.string = f'\n{INDENT_LEN_3}* {lines[0]}'
                 if len(lines) > 1:
-                    tag.string += indent('\n' + '\n'.join(lines[1:]), INDENT + '  ')
+                    tag.string += indent('\n' + '\n'.join(lines[1:]), INDENT_LEN_3 + '  ')
 
     # format elements in the `Option:` section by adding new line and indent
     for tag in html_elements.select('kbd'):
-        tag.insert_before(f'\n{INDENT}')
+        tag.insert_before(f'\n{INDENT_LEN_3}')
 
     # add indent to the description list
     for tag in html_elements.select('dt'):
-        tag.string = '\n' + indent(tag.text.strip(), INDENT)
+        tag.string = '\n' + indent(tag.text.strip(), INDENT_LEN_3)
 
     # add indent to the nested description list
     for tag in html_elements.select('dd'):
-        tag.string = '\n' + indent(tag.text, 2 * INDENT) + '\n'
+        tag.string = '\n' + indent(tag.text, 2 * INDENT_LEN_3) + '\n'
 
     # add new line and indent for the cells inside a table (e.g. options inside **Options:** section)
     for tag in html_elements.select('td'):
         if 'class' not in tag.attrs:
-            tag.string = indent(f'\n{tag.text}', 2 * INDENT)
+            tag.string = indent(f'\n{tag.text}', 2 * INDENT_LEN_3)
 
     # format code examples
     for tag in html_elements.select('pre'):
@@ -115,7 +118,7 @@ def html2txt(html: str):
             if ' '.join(tag.attrs['class']).replace('first', '').replace('last', '').replace('  ', ' ') == 'code literal-block':
                 tag.string = '\n<dim>' + indent(tag.text, '  ') + '</dim>'
             elif 'literal-block' in ' '.join(tag.attrs['class']):
-                tag.string = '<b>' + indent(tag.text.rstrip('\n'), INDENT) + '</b>'
+                tag.string = '<b>' + indent(tag.text.rstrip('\n'), INDENT_LEN_3) + '</b>'
 
     # build python docs string out of the previously formatted html tags
     text: str = ''
@@ -125,22 +128,45 @@ def html2txt(html: str):
                 new_text = html_element
             else:
                 new_text = ''
-            text += new_text
-    return text
 
+            # keep exactly one line gap between points in bulleted list
+            if f'{INDENT_LEN_3}- ' in html_element and f'{INDENT_LEN_3}* ' in html_element:
+                new_text = html_element.rstrip()
 
-def skip_or_replace_unparseable_directives(rst: str) -> str:
-    rst = rst.replace(':ref:', '')
-    return rst
+            text += new_text
+    return text
 
 
-def resolve_includes(rst: str, docs_source_path: str) -> str:
-    matches = re.findall(r'(.*)\.\. include:: (.*)', rst)
-    for indent_, match in matches:
-        with open(f'{docs_source_path}/{match}', 'r') as handle:
-            include_text = handle.read()
-        rst = rst.replace(f'{indent_}.. include:: {match}', indent(dedent(include_text), indent_))
-    return rst
+def skip_or_replace_unparseable_directives(rst_: str) -> str:
+    rst_ = rst_.replace(':ref:', '')
+    return rst_
+
+
+def resolve_includes(rst_: str, docs_source_path_: str) -> str:
+    matches = re.findall(r'(.*)\.\. include:: (.*)\n(.* :(.*): ([0-9]*)\n)?(.* :(.*): ([0-9]*)\n)?', rst_)
+    # example matches:
+    #     .. include:: status_config_key.rst
+    #
+    #     .. include:: status_config_key.rst
+    #         :start-line: 2
+    #
+    #     .. include:: status_config_key.rst
+    #         :start-line: 2
+    #         :end-line: 6
+    for indent_, match, option_1_str, option_1, option_1_value, option_2_str, option_2, option_2_value in matches:
+        with open(f'{docs_source_path_}/{match}', 'r') as handle:
+            include_text = handle.readlines()
+        text_to_replace = f'{indent_}.. include:: {match}'
+        start_line, end_line = 0, len(include_text)
+        if option_1 == 'start-line':
+            start_line = int(option_1_value)
+            text_to_replace += f'\n{option_1_str}'
+        if option_2 == 'end-line':
+            end_line = int(option_2_value)
+            text_to_replace += f'{option_2_str}'
+        include_text = include_text[start_line:end_line]
+        rst_ = rst_.replace(text_to_replace, indent(dedent(''.join(include_text)), indent_), 1)
+    return rst_
 
 
 def skip_prefix_new_lines(txt: str) -> str:
@@ -155,12 +181,31 @@ def replace_3_newlines_and_more_with_2_newlines(txt: str) -> str:
 
 
 if __name__ == '__main__':
+    INDENT_LEN_3 = 3 * ' '
+    INDENT_LEN_4 = 4 * ' '
     docs_source_path = 'docs/source'
     warning_text = '# ---------------------------------------------------------------------------------------------------------\n' \
                    '# Warning: This file is NOT supposed to be edited directly, ' \
-                   'but instead regenerated via tox -e docs\n' \
+                   'but instead regenerated via `tox -e py-docs`\n' \
                    '# ---------------------------------------------------------------------------------------------------------\n'
-    output_text = 'from typing import Dict\n\n' + warning_text + '\nlong_docs: Dict[str, str] = {\n'
+    output_text = 'from typing import Dict\n\n' + warning_text
+
+    # build short docs
+    output_text += '\nshort_docs: Dict[str, str] = {\n'
+    short_docs_rst_file = docs_source_path + '/short_docs.rst'
+    with open(short_docs_rst_file, 'r') as f:
+        rst = f.read()
+    rst = skip_or_replace_unparseable_directives(rst)
+    html = rst2html(rst)
+    plain_text = html2txt(html)
+    plain_text = skip_prefix_new_lines(plain_text)
+    command_and_short_docs = re.findall(r'\* `([a-z-]*)`.*-- (.*)', plain_text)
+    for command, short_doc in command_and_short_docs:
+        output_text += f'{INDENT_LEN_4}"{command}": "{short_doc}",\n'
+    output_text += '}'
+
+    # build long docs
+    output_text += '\n\nlong_docs: Dict[str, str] = {\n'
     path = docs_source_path + '/cli_help'
     commands_and_file_paths = {f.split('.')[0]: join(path, f) for f in sorted(os.listdir(path)) if isfile(join(path, f))}
 
@@ -169,7 +214,7 @@ if __name__ == '__main__':
             rst = f.read()
 
         rst = skip_or_replace_unparseable_directives(rst)
-        rst = resolve_includes(rst=rst, docs_source_path=docs_source_path)
+        rst = resolve_includes(rst_=rst, docs_source_path_=docs_source_path)
 
         html = rst2html(rst)
 
@@ -177,7 +222,7 @@ if __name__ == '__main__':
         plain_text = skip_prefix_new_lines(plain_text)
         plain_text = plain_text.replace('---', '—')
 
-        output_text += f'    "{command}": """\n' + indent(plain_text, '        ') + '\n   """,\n'
+        output_text += f'{INDENT_LEN_4}"{command}": """\n' + indent(plain_text, 2 * INDENT_LEN_4) + '\n   """,\n'
         output_text = replace_3_newlines_and_more_with_2_newlines(output_text)
 
     output_text += '}'
diff --git a/docs/source/cli.rst b/docs/source/cli.rst
index 9397bb4..fc83181 100644
--- a/docs/source/cli.rst
+++ b/docs/source/cli.rst
@@ -9,33 +9,7 @@ When git-machete is installed, it adds ``machete`` command to ``git``, so it can
 
 git machete commands and help topics:
 
-* :ref:`add`              -- Add a branch to the tree of branch dependencies.
-* :ref:`advance`          -- Fast-forward merge one of children to the current branch and then slide out this child
-* :ref:`anno`             -- Manage custom annotations
-* :ref:`config`           -- Display docs for config keys and environment variables
-* :ref:`clean`            -- Delete untracked and unmanaged branches and also optionally check out user's open GitHub PRs
-* :ref:`delete-unmanaged` -- Delete local branches that are not present in the definition file
-* :ref:`diff`             -- Diff current working directory or a given branch against its computed fork point
-* :ref:`discover`         -- Automatically discover tree of branch dependencies
-* :ref:`edit`             -- Edit the definition file
-* :ref:`file`             -- Display the location of the definition file
-* :ref:`fork-point`       -- Display or override fork point for a branch
-* :ref:`format`           -- Display docs for the format of the definition file
-* :ref:`github`           -- Create, check out and manage GitHub PRs while keeping them reflected in git machete
-* :ref:`go`               -- Check out the branch relative to the position of the current branch, accepts down/first/last/next/root/prev/up <annotation text>
-* :ref:`help`             -- Display this overview, or detailed help for a specified command
-* :ref:`hooks`            -- Display docs for the extra hooks added by git machete
-* :ref:`is-managed`       -- Check if the current branch is managed by git machete (mostly for scripts)
-* :ref:`list`             -- List all branches that fall into one of pre-defined categories (mostly for internal use)
-* :ref:`log`              -- Log the part of history specific to the given branch
-* :ref:`reapply`          -- Rebase the current branch onto its computed fork point
-* :ref:`show`             -- Show name(s) of the branch(es) relative to the position of a branch, accepts down/first/last/next/root/prev/up <annotation text>
-* :ref:`slide-out`        -- Slide out the current branch and sync its downstream (child) branches with its upstream (parent) branch via rebase or merge
-* :ref:`squash`           -- Squash the unique history of the current branch into a single commit
-* :ref:`status`           -- Display formatted tree of branch dependencies, including info on their sync with upstream branch and with remote
-* :ref:`traverse`         -- Walk through the tree of branch dependencies and rebase, merge, slide out, push and/or pull each branch one by one
-* :ref:`update`           -- Sync the current branch with its upstream (parent) branch via rebase or merge
-* :ref:`version`          -- Display the version and exit
+.. include:: short_docs.rst
 
 To get help via CLI run:
 
diff --git a/docs/source/cli_help/config.rst b/docs/source/cli_help/config.rst
index df8bb6d..a4470b9 100644
--- a/docs/source/cli_help/config.rst
+++ b/docs/source/cli_help/config.rst
@@ -2,16 +2,17 @@
 
 config
 ------
-Documentation about available ``git machete`` config keys and environment variables that change the command's default behavior.
+Documentation about available ``git machete`` git config keys and environment variables that change the command's default behavior.
 
 Note: ``config`` is not a command as such, just a help topic (there is no ``git machete config`` command).
 
-**Config keys:**
+**Git config keys:**
 
 ``machete.github.{remote,organization,repository}``:
     When executing ``git machete github <subcommand>`` command, the following will happen:
 
     .. include:: github_config_keys.rst
+        :start-line: 2
 
 ``machete.overrideForkPoint.<branch>.{to,whileDescendantOf}``:
     Executing ``git machete fork-point --override-to=<revision> [<branch>]`` sets up a fork point override for <branch>.
@@ -20,6 +21,11 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
 
 ``machete.status.extraSpaceBeforeBranchName``:
     .. include:: status_config_key.rst
+        :start-line: 2
+
+``machete.traverse.push``:
+    .. include:: traverse_config_key.rst
+        :start-line: 2
 
 ``machete.worktree.useTopLevelMacheteFile``:
     The default value of this key is ``true``, which means that the path to machete definition file will be ``.git/machete``
diff --git a/docs/source/cli_help/status.rst b/docs/source/cli_help/status.rst
index e83cf23..415be80 100644
--- a/docs/source/cli_help/status.rst
+++ b/docs/source/cli_help/status.rst
@@ -71,6 +71,7 @@ With ``--color=always``, git machete always emits colors and with ``--color=auto
     |
     m-<branch4>     # grey (merged to parent)
 
+.. include:: status_config_key.rst
 
 **Options:**
 
@@ -87,3 +88,5 @@ With ``--color=always``, git machete always emits colors and with ``--color=auto
 
 ``machete.status.extraSpaceBeforeBranchName``
     .. include:: status_config_key.rst
+        :start-line: 2
+        :end-line: 6
diff --git a/docs/source/cli_help/traverse.rst b/docs/source/cli_help/traverse.rst
index f3d3b0a..2ecb0ac 100644
--- a/docs/source/cli_help/traverse.rst
+++ b/docs/source/cli_help/traverse.rst
@@ -56,6 +56,10 @@ For each branch, the command:
 
       - prints the updated ``status``.
 
+By default ``traverse`` asks if the branch should be pushed, this behaviour can, however, be changed with the ``machete.traverse.push`` configuration key.
+It can also be customized using options: ``--[no-]push`` or ``--[no-]push-untracked`` --- the order of the flags defines their precedence over each other
+(the one on the right overriding the ones on the left). More on them in the **Options** section below.
+
 If the traverse flow is stopped (typically due to merge/rebase conflicts), just run ``git machete traverse`` after the merge/rebase is finished.
 It will pick up the walk from the current branch (unless ``--start-from=`` or ``-w`` etc. is passed).
 Unlike with e.g. ``git rebase``, there is no special ``--continue`` flag, as ``traverse`` is stateless
@@ -84,9 +88,11 @@ Unlike with e.g. ``git rebase``, there is no special ``--continue`` flag, as ``t
 
 --no-push-untracked          Do not push untracked branches to remote, re-enable via ``--push-untracked``.
 
---push                       Push all (both tracked and untracked) branches to remote --- default behavior.
+--push                       Push all (both tracked and untracked) branches to remote --- default behavior. Default behaviour can be changed
+                             by setting git configuration key ``git config machete.traverse.push false``.
+                             Configuration key value can be overridden by the presence of the flag.
 
---push-untracked             Push untracked branches to remote --- default behavior.
+--push-untracked             Push untracked branches to remote.
 
 --return-to=WHERE            Specifies the branch to return after traversal is successfully completed;
                              WHERE can be ``here`` (the current branch at the moment when traversal starts), ``nearest-remaining``
@@ -106,6 +112,12 @@ Unlike with e.g. ``git rebase``, there is no special ``--continue`` flag, as ``t
 
 -y, --yes                    Don't ask for any interactive input, including confirmation of rebase/push/pull. Implies ``-n``.
 
+**Config keys:**
+
+``machete.traverse.push``
+    .. include:: traverse_config_key.rst
+        :start-line: 2
+
 **Environment variables:**
 
 ``GIT_MACHETE_REBASE_OPTS``
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 5fc2014..579fd6d 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -49,7 +49,8 @@ exclude_patterns = [
     'learning_materials.rst',
     'github_api_access.rst',
     'github_config_keys.rst',
-    'status_config_key.rst'
+    'status_config_key.rst',
+    'traverse_config_key.rst'
 ]
 
 
diff --git a/docs/source/github_api_access.rst b/docs/source/github_api_access.rst
index e0b9b0b..1e53040 100644
--- a/docs/source/github_api_access.rst
+++ b/docs/source/github_api_access.rst
@@ -1,4 +1,4 @@
- .. note::
+.. note::
 
     To allow GitHub API access for private repositories (and also to perform side-effecting actions like opening a PR,
     even in case of public repositories), a GitHub API token with ``repo`` scope is required, see https://github.com/settings/tokens.
diff --git a/docs/source/github_config_keys.rst b/docs/source/github_config_keys.rst
index da228e1..107dfb6 100644
--- a/docs/source/github_config_keys.rst
+++ b/docs/source/github_config_keys.rst
@@ -1,4 +1,4 @@
- .. note::
+.. note::
 
     GitHub API server URL will be inferred from ``git remote``.
     You can override this by setting the following git config keys:
@@ -20,3 +20,7 @@
             organization = <organization_name>
             repository = <repo_name>
             remote = <remote_name>
+
+..
+    Text order in this file is relevant, if you want to change something, find each ``.. include:: status_config_key.rst`` instance
+    and if the instance has ``start-line`` or ``end-line`` options provided, make sure that after changes the output text stays the same.
diff --git a/docs/source/short_docs.rst b/docs/source/short_docs.rst
new file mode 100644
index 0000000..ab167ad
--- /dev/null
+++ b/docs/source/short_docs.rst
@@ -0,0 +1,29 @@
+.. _short_docs:
+
+* :ref:`add`              -- Add a branch to the tree of branch dependencies
+* :ref:`advance`          -- Fast-forward merge one of children to the current branch, push it and then slide out the child
+* :ref:`anno`             -- Manage custom annotations
+* :ref:`clean`            -- Delete untracked and unmanaged branches and also optionally check out user's open GitHub PRs
+* :ref:`config`           -- Display docs for the git machete configuration keys and environment variables
+* :ref:`delete-unmanaged` -- Delete local branches that are not present in the definition file
+* :ref:`diff`             -- Diff current working directory or a given branch against its computed fork point
+* :ref:`discover`         -- Automatically discover tree of branch dependencies
+* :ref:`edit`             -- Edit the definition file
+* :ref:`file`             -- Display the location of the definition file
+* :ref:`fork-point`       -- Display or override fork point for a branch
+* :ref:`format`           -- Display docs for the format of the definition file
+* :ref:`github`           -- Create, check out and manage GitHub PRs while keeping them reflected in git machete
+* :ref:`go`               -- Check out the branch relative to the position of the current branch, accepts down/first/last/next/root/prev/up argument
+* :ref:`help`             -- Display this overview, or detailed help for a specified command
+* :ref:`hooks`            -- Display docs for the extra hooks added by git machete
+* :ref:`is-managed`       -- Check if the current branch is managed by git machete (mostly for scripts)
+* :ref:`list`             -- List all branches that fall into one of pre-defined categories (mostly for internal use)
+* :ref:`log`              -- Log the part of history specific to the given branch
+* :ref:`reapply`          -- Rebase the current branch onto its computed fork point
+* :ref:`show`             -- Show name(s) of the branch(es) relative to the position of a branch, accepts down/first/last/next/root/prev/up argument
+* :ref:`slide-out`        -- Slide out the current branch and sync its downstream (child) branches with its upstream (parent) branch via rebase or merge
+* :ref:`squash`           -- Squash the unique history of the current branch into a single commit
+* :ref:`status`           -- Display formatted tree of branch dependencies, including info on their sync with upstream branch and with remote
+* :ref:`traverse`         -- Walk through the tree of branch dependencies and rebase, merge, slide out, push and/or pull each branch one by one. By default starts from current branch
+* :ref:`update`           -- Sync the current branch with its upstream (parent) branch via rebase or merge
+* :ref:`version`          -- Display the version and exit
diff --git a/docs/source/status_config_key.rst b/docs/source/status_config_key.rst
index c5459f9..4a8e9de 100644
--- a/docs/source/status_config_key.rst
+++ b/docs/source/status_config_key.rst
@@ -1,8 +1,8 @@
- .. note::
+.. note::
 
     To make it easier to select branch name from the ``status`` output on certain terminals
-    (e.g. `Alacritty <https://github.com/alacritty/alacritty>`_), you can add an extra
-    space between └─ and ``branch name`` by setting ``git config machete.status.extraSpaceBeforeBranchName true``.
+    (e.g. `Alacritty <https://github.com/alacritty/alacritty>`_), you can add an extra space between └─ and ``branch name``
+    by setting ``git config machete.status.extraSpaceBeforeBranchName true``.
 
     For example, by default the status is displayed as:
 
@@ -23,3 +23,7 @@
        ├─ feature_branch1
        │
        └─ feature_branch2
+
+..
+    Text order in this file is relevant, if you want to change something, find each ``.. include:: status_config_key.rst`` instance
+    and if the instance has ``start-line`` or ``end-line`` options provided, make sure that after changes the output text stays the same.
diff --git a/docs/source/traverse_config_key.rst b/docs/source/traverse_config_key.rst
new file mode 100644
index 0000000..a454278
--- /dev/null
+++ b/docs/source/traverse_config_key.rst
@@ -0,0 +1,9 @@
+.. note::
+
+    To change the behaviour of ``git machete traverse`` command so that it doesn't push branches by default,
+    you need to set config key ``git config machete.traverse.push false``.
+    Configuration key value can be overridden by the presence of the flag.
+
+..
+    Text order in this file is relevant, if you want to change something, find each ``.. include:: status_config_key.rst`` instance
+    and if the instance has ``start-line`` or ``end-line`` options provided, make sure that after changes the output text stays the same.
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 4057eed..8576e6e 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.12.5'
+__version__ = '3.13.0'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 4b3d64b..444fc10 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -11,9 +11,8 @@ import git_machete.options
 from git_machete import __version__
 from git_machete import utils
 from git_machete.client import MacheteClient
-from git_machete.generated_long_docs import long_docs
-from git_machete.handmade_short_docs import short_docs
-from git_machete.exceptions import MacheteException, StopInteraction
+from git_machete.generated_docs import short_docs, long_docs
+from git_machete.exceptions import ExitCode, MacheteException, StopInteraction
 from git_machete.git_operations import AnyBranchName, AnyRevision, GitContext, LocalBranchShortName, RemoteBranchShortName
 from git_machete.utils import bold, fmt, underline, excluding, warn
 
@@ -30,6 +29,8 @@ alias_by_command: Dict[str, str] = {
     "traverse": "t"
 }
 
+command_by_alias: Dict[str, str] = {v: k for k, v in alias_by_command.items()}
+
 command_groups: List[Tuple[str, List[str]]] = [
     ("General topics",
      ["config", "file", "format", "help", "hooks", "version"]),
@@ -45,11 +46,16 @@ command_groups: List[Tuple[str, List[str]]] = [
      ["github"])
 ]
 
+commands_and_aliases = list(long_docs.keys()) + list(alias_by_command.keys())
+
 
 def get_help_description(command: str = None) -> str:
     usage_str = ''
-    if command and command in long_docs:
-        usage_str += fmt(textwrap.dedent(long_docs[command]))
+    if command and command in commands_and_aliases:
+        if command in long_docs:
+            usage_str += fmt(textwrap.dedent(long_docs[command]))
+        elif command in command_by_alias:
+            usage_str += fmt(textwrap.dedent(long_docs[command_by_alias[command]]))
     else:
         usage_str += get_short_general_usage() + '\n'
         if command:
@@ -109,7 +115,7 @@ class MacheteHelpAction(argparse.Action):
         # parser name (prog) is expected to be `git machete` or `git machete <command>`
         command_name = parser.prog.replace('git machete', '').strip()
         print(get_help_description(command_name))
-        parser.exit(status=None)
+        parser.exit(status=ExitCode.SUCCESS)
 
 
 def create_cli_parser() -> argparse.ArgumentParser:
@@ -240,7 +246,7 @@ def create_cli_parser() -> argparse.ArgumentParser:
 
     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()))
+    help_parser.add_argument('topic_or_cmd', nargs='?', choices=commands_and_aliases)
 
     is_managed_parser = subparsers.add_parser(
         'is-managed',
@@ -453,6 +459,18 @@ def update_cli_opts_using_parsed_args(
         cli_opts.opt_no_interactive_rebase = True
 
 
+def update_cli_options_using_config_keys(
+        cli_opts: git_machete.options.CommandLineOptions,
+        git: GitContext
+) -> None:
+    machete_traverse_push_config_key = git.get_boolean_config_attr_or_none(key='machete.traverse.push')
+    if machete_traverse_push_config_key is not None:
+        if machete_traverse_push_config_key:
+            cli_opts.opt_push_tracked, cli_opts.opt_push_untracked = True, True
+        else:
+            cli_opts.opt_push_tracked, cli_opts.opt_push_untracked = False, False
+
+
 def set_utils_global_variables(
         cli_opts: git_machete.options.CommandLineOptions) -> None:
     if cli_opts.opt_color:
@@ -471,7 +489,7 @@ def get_local_branch_short_name_from_arg(
     return LocalBranchShortName.of(branch_from_arg.replace('refs/heads/', '')) if branch_from_arg else None
 
 
-def exit_script(status_code: Optional[int] = None, error: Optional[BaseException] = None) -> None:
+def exit_script(status_code: int, error: Optional[BaseException] = None) -> None:
     # Single point of exit is useful, because we can mock this method in tests
     # and verify that actual errors like MacheteException are raised with
     # appropriate messages. Otherwise it's not possible, because SystemError
@@ -493,22 +511,24 @@ def launch(orig_args: List[str]) -> None:
         parsed_cli: argparse.Namespace = cli_parser.parse_args(orig_args)
         parsed_cli_as_dict: Dict[str, str] = vars(parsed_cli)
 
+        update_cli_options_using_config_keys(cli_opts, git)
+
         update_cli_opts_using_parsed_args(cli_opts, parsed_cli)
         cli_opts.validate()
         set_utils_global_variables(cli_opts)
 
         if not orig_args:
             print(get_help_description())
-            exit_script(2)
+            exit_script(ExitCode.ARGUMENT_ERROR)
 
         cmd = parsed_cli.command
 
         if cmd == "help":
             print(get_help_description(parsed_cli.topic_or_cmd))
-            exit_script()
+            exit_script(ExitCode.SUCCESS)
         elif cmd == "version":
             version()
-            exit_script()
+            exit_script(ExitCode.SUCCESS)
 
         machete_client = MacheteClient(git)
 
@@ -664,7 +684,7 @@ def launch(orig_args: List[str]) -> None:
             machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             branch = get_local_branch_short_name_from_arg_or_current_branch(cli_opts.opt_branch, git)
             if branch is None or branch not in machete_client.managed_branches:
-                exit_script(1)
+                exit_script(ExitCode.MACHETE_EXCEPTION)
         elif cmd == "list":
             category = parsed_cli.category
             if category == 'slidable-after' and 'branch' not in parsed_cli_as_dict:
@@ -811,13 +831,15 @@ def launch(orig_args: List[str]) -> None:
                 opt_no_interactive_rebase=cli_opts.opt_no_interactive_rebase,
                 opt_fork_point=cli_opts.opt_fork_point)
 
+    except EOFError:
+        exit_script(ExitCode.END_OF_FILE_SIGNAL)
     except KeyboardInterrupt:
-        exit_script(3)
+        exit_script(ExitCode.KEYBOARD_INTERRUPT)
     except (argparse.ArgumentError, argparse.ArgumentTypeError) as e:
         print(get_short_general_usage())
-        exit_script(2, e)
+        exit_script(ExitCode.ARGUMENT_ERROR, e)
     except MacheteException as e:
-        exit_script(1, e)
+        exit_script(ExitCode.MACHETE_EXCEPTION, e)
     except StopInteraction:
         pass
     finally:
diff --git a/git_machete/exceptions.py b/git_machete/exceptions.py
index b0f5d1c..0ea6a52 100644
--- a/git_machete/exceptions.py
+++ b/git_machete/exceptions.py
@@ -1,3 +1,5 @@
+from enum import IntEnum
+
 from git_machete import utils
 
 
@@ -26,3 +28,11 @@ class UnprocessableEntityHTTPError(MacheteException):
 
     def __str__(self) -> str:
         return str(self.msg)
+
+
+class ExitCode(IntEnum):
+    SUCCESS = 0
+    MACHETE_EXCEPTION = 1
+    ARGUMENT_ERROR = 2
+    KEYBOARD_INTERRUPT = 3
+    END_OF_FILE_SIGNAL = 4
diff --git a/git_machete/generated_long_docs.py b/git_machete/generated_docs.py
similarity index 91%
rename from git_machete/generated_long_docs.py
rename to git_machete/generated_docs.py
index 14b6159..903b3e5 100644
--- a/git_machete/generated_long_docs.py
+++ b/git_machete/generated_docs.py
@@ -1,9 +1,39 @@
 from typing import Dict
 
 # ---------------------------------------------------------------------------------------------------------
-# Warning: This file is NOT supposed to be edited directly, but instead regenerated via tox -e docs
+# Warning: This file is NOT supposed to be edited directly, but instead regenerated via `tox -e py-docs`
 # ---------------------------------------------------------------------------------------------------------
 
+short_docs: Dict[str, str] = {
+    "add": "Add a branch to the tree of branch dependencies",
+    "advance": "Fast-forward merge one of children to the current branch, push it and then slide out the child",
+    "anno": "Manage custom annotations",
+    "clean": "Delete untracked and unmanaged branches and also optionally check out user's open GitHub PRs",
+    "config": "Display docs for the git machete configuration keys and environment variables",
+    "delete-unmanaged": "Delete local branches that are not present in the definition file",
+    "diff": "Diff current working directory or a given branch against its computed fork point",
+    "discover": "Automatically discover tree of branch dependencies",
+    "edit": "Edit the definition file",
+    "file": "Display the location of the definition file",
+    "fork-point": "Display or override fork point for a branch",
+    "format": "Display docs for the format of the definition file",
+    "github": "Create, check out and manage GitHub PRs while keeping them reflected in git machete",
+    "go": "Check out the branch relative to the position of the current branch, accepts down/first/last/next/root/prev/up argument",
+    "help": "Display this overview, or detailed help for a specified command",
+    "hooks": "Display docs for the extra hooks added by git machete",
+    "is-managed": "Check if the current branch is managed by git machete (mostly for scripts)",
+    "list": "List all branches that fall into one of pre-defined categories (mostly for internal use)",
+    "log": "Log the part of history specific to the given branch",
+    "reapply": "Rebase the current branch onto its computed fork point",
+    "show": "Show name(s) of the branch(es) relative to the position of a branch, accepts down/first/last/next/root/prev/up argument",
+    "slide-out": "Slide out the current branch and sync its downstream (child) branches with its upstream (parent) branch via rebase or merge",
+    "squash": "Squash the unique history of the current branch into a single commit",
+    "status": "Display formatted tree of branch dependencies, including info on their sync with upstream branch and with remote",
+    "traverse": "Walk through the tree of branch dependencies and rebase, merge, slide out, push and/or pull each branch one by one. By default starts from current branch",
+    "update": "Sync the current branch with its upstream (parent) branch via rebase or merge",
+    "version": "Display the version and exit",
+}
+
 long_docs: Dict[str, str] = {
     "add": """
         <b>Usage:</b><b>
@@ -191,11 +221,11 @@ long_docs: Dict[str, str] = {
 
    """,
     "config": """
-        Documentation about available `git machete` config keys and environment variables that change the command's default behavior.
+        Documentation about available `git machete` git config keys and environment variables that change the command's default behavior.
 
         Note: `config` is not a command as such, just a help topic (there is no `git machete config` command).
 
-        <b>Config keys:</b>
+        <b>Git config keys:</b>
            `machete.github.{remote,organization,repository}`:
  
               When executing `git machete github <subcommand>` command, the following will happen:
@@ -227,8 +257,8 @@ long_docs: Dict[str, str] = {
            `machete.status.extraSpaceBeforeBranchName`:
 
               To make it easier to select branch name from the `status` output on certain terminals
-              (e.g. Alacritty), you can add an extra
-              space between └─ and `branch name` by setting `git config machete.status.extraSpaceBeforeBranchName true`.
+              (e.g. Alacritty), you can add an extra space between └─ and `branch name`
+              by setting `git config machete.status.extraSpaceBeforeBranchName true`.
 
               For example, by default the status is displayed as:
 
@@ -246,6 +276,12 @@ long_docs: Dict[str, str] = {
               │
               └─ feature_branch2
 
+           `machete.traverse.push`:
+
+              To change the behaviour of `git machete traverse` command so that it doesn't push branches by default,
+              you need to set config key `git config machete.traverse.push false`.
+              Configuration key value can be overridden by the presence of the flag.
+
            `machete.worktree.useTopLevelMacheteFile`:
               The default value of this key is `true`, which means that the path to machete definition file will be `.git/machete`
               for both regular directory and worktree. If you want the worktree to have its own machete definition file (located under
@@ -363,7 +399,6 @@ long_docs: Dict[str, str] = {
              the file path depends on the `machete.worktree.useTopLevelMacheteFile` config key value:
               - if `machete.worktree.useTopLevelMacheteFile` is true (default), the file is located under `.git/machete`
               - if `machete.worktree.useTopLevelMacheteFile` is false, the file is located under `.git/worktrees/.../machete`,
-
            * if `git machete` is executed from a <b>submodule</b>, this file is located in the git folder of the submodule itself under `.git/modules/.../machete`.
    """,
     "fork-point": """
@@ -830,7 +865,6 @@ long_docs: Dict[str, str] = {
                 and the fork point of the downstream branch is equal to the upstream branch tip,
               - grey/dimmed edge means that the downstream branch has been merged to the upstream branch,
                 detected by commit equivalency (default), or by strict detection of merge commits (if `--no-detect-squash-merges` passed).
-
            * prints (`untracked`/`ahead of <remote>`/`behind <remote>`/`diverged from [& older than] <remote>`) message if the branch
              is not in sync with its remote counterpart;
            * displays the custom annotations (see help for `format` and `anno`) next to each branch, if present;
@@ -860,6 +894,28 @@ long_docs: Dict[str, str] = {
           m-<branch4>     # grey (merged to parent)
         </dim>
 
+        To make it easier to select branch name from the `status` output on certain terminals
+        (e.g. Alacritty), you can add an extra space between └─ and `branch name`
+        by setting `git config machete.status.extraSpaceBeforeBranchName true`.
+
+        For example, by default the status is displayed as:
+        <dim>
+          develop
+          │
+          ├─feature_branch1
+          │
+          └─feature_branch2
+        </dim>
+
+        With `machete.status.extraSpaceBeforeBranchName` config set to `true`:
+        <dim>
+          develop
+          │
+          ├─ feature_branch1
+          │
+          └─ feature_branch2
+        </dim>
+
         <b>Options:</b>
            <b>--color=WHEN</b>
               Colorize the output; WHEN can be `always`, `auto` (default; i.e. only if stdout is a terminal), or `never`.
@@ -873,26 +929,9 @@ long_docs: Dict[str, str] = {
 
         <b>Config keys:</b>
            `machete.status.extraSpaceBeforeBranchName`
-
               To make it easier to select branch name from the `status` output on certain terminals
-              (e.g. Alacritty), you can add an extra
-              space between └─ and `branch name` by setting `git config machete.status.extraSpaceBeforeBranchName true`.
-
-              For example, by default the status is displayed as:
-
-              develop
-              │
-              ├─feature_branch1
-              │
-              └─feature_branch2
-
-              With `machete.status.extraSpaceBeforeBranchName` config set to `true`:
-
-              develop
-              │
-              ├─ feature_branch1
-              │
-              └─ feature_branch2
+              (e.g. Alacritty), you can add an extra space between └─ and `branch name`
+              by setting `git config machete.status.extraSpaceBeforeBranchName true`.
 
    """,
     "traverse": """
@@ -910,24 +949,23 @@ long_docs: Dict[str, str] = {
            * detects if the branch is merged (grey edge) to its parent (aka upstream):
               - by commit equivalency (default), or by strict detection of merge commits (if `--no-detect-squash-merges` passed),
               - if so, asks the user whether to slide out the branch from the dependency tree (typically branches are no longer needed after they're merged);
-
            * otherwise, if the branch has a <red>red</red> or <yellow>yellow</yellow> edge to its parent/upstream (see help for `status`):
               - asks the user whether to rebase (default) or merge (if `--merge` passed) the branch onto into its upstream branch
                 — equivalent to `git machete update` with no `--fork-point` option passed;
-
            * if the branch is not tracked on a remote, is ahead of its remote counterpart, or diverged from the counterpart &
              has newer head commit than the counterpart:
               - asks the user whether to push the branch (possibly with `--force-with-lease` if the branches diverged);
-
            * otherwise, if the branch diverged from the remote counterpart & has older head commit than the counterpart:
               - asks the user whether to `git reset --keep` the branch to its remote counterpart
-
            * otherwise, if the branch is behind its remote counterpart:
               - asks the user whether to pull the branch;
-
            * and finally, if any of the above operations has been successfully completed:
               - prints the updated `status`.
 
+        By default `traverse` asks if the branch should be pushed, this behaviour can, however, be changed with the `machete.traverse.push` configuration key.
+        It can also be customized using options: `--[no-]push` or `--[no-]push-untracked` — the order of the flags defines their precedence over each other
+        (the one on the right overriding the ones on the left). More on them in the <b>Options</b> section below.
+
         If the traverse flow is stopped (typically due to merge/rebase conflicts), just run `git machete traverse` after the merge/rebase is finished.
         It will pick up the walk from the current branch (unless `--start-from=` or `-w` etc. is passed).
         Unlike with e.g. `git rebase`, there is no special `--continue` flag, as `traverse` is stateless
@@ -956,9 +994,11 @@ long_docs: Dict[str, str] = {
            <b>--no-push-untracked</b>
               Do not push untracked branches to remote, re-enable via `--push-untracked`.
            <b>--push</b>
-              Push all (both tracked and untracked) branches to remote — default behavior.
+              Push all (both tracked and untracked) branches to remote — default behavior. Default behaviour can be changed
+              by setting git configuration key `git config machete.traverse.push false`.
+              Configuration key value can be overridden by the presence of the flag.
            <b>--push-untracked</b>
-              Push untracked branches to remote — default behavior.
+              Push untracked branches to remote.
            <b>--return-to=WHERE</b>
               Specifies the branch to return after traversal is successfully completed;
               WHERE can be `here` (the current branch at the moment when traversal starts), `nearest-remaining`
@@ -978,6 +1018,13 @@ long_docs: Dict[str, str] = {
            <b>-y</b>, <b>--yes</b>
               Don't ask for any interactive input, including confirmation of rebase/push/pull. Implies `-n`.
 
+        <b>Config keys:</b>
+           `machete.traverse.push`
+
+              To change the behaviour of `git machete traverse` command so that it doesn't push branches by default,
+              you need to set config key `git config machete.traverse.push false`.
+              Configuration key value can be overridden by the presence of the flag.
+
         <b>Environment variables:</b>
            `GIT_MACHETE_REBASE_OPTS`
               Extra options to pass to the underlying `git rebase` invocations, space-separated.
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 43e89db..e95824a 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -347,10 +347,15 @@ class GitContext:
         return self.__config_cached.get(key.lower())
 
     def get_boolean_config_attr(self, key: str, default_value: bool) -> bool:
+        if self.get_boolean_config_attr_or_none(key) is not None:
+            return self.get_boolean_config_attr_or_none(key)
+        return default_value
+
+    def get_boolean_config_attr_or_none(self, key: str) -> Optional[bool]:
         self.__ensure_config_loaded()
         if self.__config_cached.get(key.lower()) is not None:
             return self.__config_cached.get(key.lower()) == 'true'
-        return default_value
+        return None
 
     def set_config_attr(self, key: str, value: str) -> None:
         self._run_git("config", "--", key, value)
diff --git a/git_machete/handmade_short_docs.py b/git_machete/handmade_short_docs.py
deleted file mode 100644
index 828cfa7..0000000
--- a/git_machete/handmade_short_docs.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from typing import Dict
-
-# ---------------------------------------------------------------------------------------------------------
-# This file is supposed to be modified manually
-# ---------------------------------------------------------------------------------------------------------
-
-short_docs: Dict[str, str] = {
-    "add": "Add a branch to the tree of branch dependencies",
-    "advance": "Fast-forward merge one of children to the current branch, push it and then slide out the child",
-    "anno": "Manage custom annotations",
-    "clean": "Delete untracked and unmanaged branches and also optionally check out user's open GitHub PRs",
-    "config": "Display docs for the git machete configuration keys and environment variables",
-    "delete-unmanaged": "Delete local branches that are not present in the definition file",
-    "diff": "Diff current working directory or a given branch against its computed fork point",
-    "discover": "Automatically discover tree of branch dependencies",
-    "edit": "Edit the definition file",
-    "file": "Display the location of the definition file",
-    "fork-point": "Display or override fork point for a branch",
-    "format": "Display docs for the format of the definition file",
-    "github": "Create, check out and manage GitHub PRs while keeping them reflected in git machete",
-    "go": "Check out the branch relative to the position of the current branch, accepts down/first/last/next/root/prev/up argument",
-    "help": "Display this overview, or detailed help for a specified command",
-    "hooks": "Display docs for the extra hooks added by git machete",
-    "is-managed": "Check if the current branch is managed by git machete (mostly for scripts)",
-    "list": "List all branches that fall into one of pre-defined categories (mostly for internal use)",
-    "log": "Log the part of history specific to the given branch",
-    "reapply": "Rebase the current branch onto its computed fork point",
-    "show": "Show name(s) of the branch(es) relative to the position of a branch, accepts down/first/last/next/root/prev/up argument",
-    "slide-out": "Slide out the current branch and sync its downstream (child) branches with its upstream (parent) branch via rebase or merge",
-    "squash": "Squash the unique history of the current branch into a single commit",
-    "status": "Display formatted tree of branch dependencies, including info on their sync with upstream branch and with remote",
-    "traverse": "Walk through the tree of branch dependencies and rebase, merge, slide out, push and/or pull each branch one by one. By default starts from current branch",
-    "update": "Sync the current branch with its upstream (parent) branch via rebase or merge",
-    "version": "Display the version and exit"
-}
diff --git a/requirements.coverage.txt b/requirements.coverage.txt
index 7112103..d197156 100644
--- a/requirements.coverage.txt
+++ b/requirements.coverage.txt
@@ -1,3 +1,3 @@
 coverage==6.5.0
-pytest==7.1.3
+pytest==7.2.0
 pytest-mock==3.10.0
diff --git a/requirements.mypy.txt b/requirements.mypy.txt
index d47f562..bc2962f 100644
--- a/requirements.mypy.txt
+++ b/requirements.mypy.txt
@@ -1,2 +1,2 @@
 mypy==0.982
-pytest==7.1.3
+pytest==7.2.0
diff --git a/requirements.sphinx-docs.txt b/requirements.sphinx-docs.txt
index c3b371b..2c673d1 100644
--- a/requirements.sphinx-docs.txt
+++ b/requirements.sphinx-docs.txt
@@ -1,2 +1,2 @@
 sphinx==5.3.0
-sphinx-rtd-theme==1.0.0
+sphinx-rtd-theme==1.1.0
diff --git a/tox.ini b/tox.ini
index 1ca63d9..42cb94c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = pep8,mypy-py{36,37,38,39,310},py{36,37,38,39,310},coverage,sphinx-docs,py-docs
+envlist = pep8,mypy-py{36,37,38,39,310,311},py{36,37,38,39,310,311},coverage,sphinx-docs,py-docs
 minversion = 2.3.2
 skipsdist = True
 
@@ -32,7 +32,7 @@ commands = flake8
 ignore = W504
 show-source = True
 max-line-length = 140
-exclude = ./.*,build,dist,*egg,venv, git_machete/handmade_short_docs.py, git_machete/generated_long_docs.py
+exclude = ./.*,build,dist,*egg,venv, git_machete/generated_docs.py
 import-order-style = pep8
 
 [testenv:venv]
@@ -69,7 +69,7 @@ allowlist_externals = bash
 deps =
   -r{toxinidir}/requirements.py-docs.txt
 commands =
-  bash -c "python docs/generate_py_docs.py > git_machete/generated_long_docs.py"
+  bash -c "python docs/generate_py_docs.py > git_machete/generated_docs.py"
 
 [testenv:check-py-docs]
 description = "Check if Python documentation is up to date with Sphinx documentation"
@@ -83,7 +83,7 @@ commands =
 
 [testenv:mypy]
 allowlist_externals = tox
-commands = tox -e "mypy-py{36,37,38,39,310}"
+commands = tox -e "mypy-py{36,37,38,39,310,311}"
 # No need to inherit deps from [testenv]
 deps =
 
@@ -93,7 +93,7 @@ deps =
 commands =
   mypy --config-file mypy.ini git_machete tests
 
-[testenv:mypy-py{37,38,39,310}]
+[testenv:mypy-py{37,38,39,310,311}]
 deps =
   -r{toxinidir}/requirements.mypy.txt
 commands =


Test patch:
diff --git a/requirements.testenv.txt b/requirements.testenv.txt
index 591c5ef..b2f90aa 100644
--- a/requirements.testenv.txt
+++ b/requirements.testenv.txt
@@ -1,3 +1,3 @@
-pytest==7.1.3
+pytest==7.2.0
 pytest-mock==3.10.0
-pytest-xdist==2.5.0
+pytest-xdist==3.0.2
diff --git a/tests/mockers.py b/tests/mockers.py
index ae0ab27..42436ec 100644
--- a/tests/mockers.py
+++ b/tests/mockers.py
@@ -406,12 +406,12 @@ def mock_should_perform_interactive_slide_out(cmd: str) -> bool:
     return True
 
 
-def mock_exit_script(status_code: Optional[int] = None, error: Optional[BaseException] = None) -> None:
+def mock_exit_script(status_code: int, error: Optional[BaseException] = None) -> None:
     if error:
         raise error
     else:
         sys.exit(status_code)
 
 
-def mock_exit_script_no_exit(status_code: Optional[int] = None, error: Optional[BaseException] = None) -> None:
+def mock_exit_script_no_exit(status_code: int, error: Optional[BaseException] = None) -> None:
     return
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..02cc3eb
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,9 @@
+from git_machete.cli import alias_by_command
+
+
+class TestCLI:
+    def test_aliases(self) -> None:
+        """
+        Verify that each command alias is unique
+        """
+        assert len(alias_by_command.values()) == len(set(alias_by_command.values()))
diff --git a/tests/test_help.py b/tests/test_help.py
index acce166..af9de4d 100644
--- a/tests/test_help.py
+++ b/tests/test_help.py
@@ -2,7 +2,8 @@ from typing import Any, List
 
 import pytest
 
-from git_machete.generated_long_docs import long_docs
+from git_machete.exceptions import ExitCode
+from git_machete.cli import commands_and_aliases
 
 from unittest import mock
 
@@ -28,39 +29,37 @@ class TestHelp:
     def test_help(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
 
-        expected_exit_code = None
-        expected_exit_code_when_failed = 2
         help_topics: List[str] = ['config', 'format', 'hooks']
 
         with pytest.raises(SystemExit) as e:
             launch_command("help")
-        assert expected_exit_code == e.value.code, \
+        assert ExitCode.SUCCESS == e.value.code, \
             "Verify that `git machete help` causes SystemExit with " \
-            f"{expected_exit_code} exit code."
+            f"{ExitCode.SUCCESS} exit code."
 
-        for command in long_docs:
+        for command in commands_and_aliases:
 
             with pytest.raises(SystemExit) as e:
                 launch_command("help", command)
-            assert expected_exit_code == e.value.code, \
+            assert ExitCode.SUCCESS == e.value.code, \
                 f"Verify that `git machete help {command}` causes SystemExit" \
-                f" with {expected_exit_code} exit code."
+                f" with {ExitCode.SUCCESS} exit code."
 
             if command not in help_topics:
                 with pytest.raises(SystemExit) as e:
                     launch_command(command, "--help")
-                assert expected_exit_code == e.value.code, \
+                assert ExitCode.SUCCESS == e.value.code, \
                     f"Verify that `git machete {command} --help` causes " \
-                    f"SystemExit with {expected_exit_code} exit code."
+                    f"SystemExit with {ExitCode.SUCCESS} exit code."
             else:
                 with pytest.raises(SystemExit) as e:
                     launch_command(command, "--help")
-                assert expected_exit_code_when_failed == e.value.code, \
+                assert ExitCode.ARGUMENT_ERROR == e.value.code, \
                     f"Verify that `git machete {command} --help` causes " \
-                    f"SystemExit with {expected_exit_code} exit code."
+                    f"SystemExit with {ExitCode.ARGUMENT_ERROR} exit code."
 
     @mock.patch('git_machete.cli.exit_script', mock_exit_script_no_exit)
     def test_help_output_has_no_ansi_codes(self) -> None:
-        for command in long_docs.keys():
+        for command in commands_and_aliases:
             help_output = launch_command('help', command)
             assert '\033' not in help_output
diff --git a/tests/test_traverse.py b/tests/test_traverse.py
index b0f43c1..d08960b 100644
--- a/tests/test_traverse.py
+++ b/tests/test_traverse.py
@@ -218,6 +218,41 @@ class TestTraverse:
             """,
         )
 
+    def test_traverse_push_config_key(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        self.setup_discover_standard_tree()
+        self.repo_sandbox.add_git_config_key('machete.traverse.push', 'false')
+        launch_command("traverse", "-Wy")
+        assert_command(
+            ["status", "-l"],
+            """
+            develop
+            |
+            | Allow ownership links
+            | 1st round of fixes
+            o-allow-ownership-link (diverged from origin)
+            | |
+            | | Build arbitrarily long chains
+            | o-build-chain (untracked)
+            |
+            | Call web service
+            | 1st round of fixes
+            | 2nd round of fixes
+            o-call-ws (ahead of origin)
+              |
+              | Drop unneeded SQL constraints
+              o-drop-constraint (untracked)
+
+            master
+            |
+            | HOTFIX Add the trigger (amended)
+            o-hotfix/add-trigger (diverged from origin)
+              |
+              | Ignore trailing data (amended)
+              o-ignore-trailing *
+            """,
+        )
+
     def test_discover_traverse_squash(self, mocker: Any) -> None:
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
         self.setup_discover_standard_tree()