Repository: VirtusLab/git-machete
Pull request #744 (2022-12-03 16:54:56)

Release v3.13.1


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5ec6d7e..36fd341 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,7 +14,6 @@ Make sure the following bundled plugins are enabled:
 
 Optionally, you can also install the following non-bundled plugins from Marketplace:
 * [AWK Support](https://plugins.jetbrains.com/plugin/17037-awk-support)
-* [.ignore](https://plugins.jetbrains.com/plugin/7495--ignore)
 * [NixIDEA](https://plugins.jetbrains.com/plugin/8607-nixidea)
 * [Requirements](https://plugins.jetbrains.com/plugin/10837-requirements/)
 
@@ -29,6 +28,7 @@ ln -s ../../hook_samples/post-commit .git/hooks/post-commit
 ln -s ../../ci/checks/run-all-checks.sh .git/hooks/pre-commit
 ```
 
+Install `fish` and `shellcheck` for `run-all-checks.sh` to pass successfully.
 
 ## Run tests locally
 
@@ -185,7 +185,7 @@ Review fixes should be pushed on separate commits for easier viewing on GitHub (
    Inspect the output of `docker-compose` and verify that the latest version gets correctly installed on Ubuntu (esp. see the output of `git machete --version`).
 
 1. Thanks to the courtesy of [@blitz (Julian Stecklina)](https://github.com/blitz),
-   a [git-machete package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/version-management/git-and-tools/git-machete/default.nix)
+   a [git-machete package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/version-management/git-machete/default.nix)
    lives in [Nixpkgs](https://github.com/NixOS/nixpkgs) &mdash; the collection of packages for [Nix package manager](https://nixos.org/).
 
    Since @blitz's [PR #131141 to NixOS/nixpkgs](https://github.com/NixOS/nixpkgs/pull/131141),
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index fa0db0c..7022eaf 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,12 @@
 # Release notes
 
+## New in git-machete 3.13.1
+
+- fixed: support GitHub remote URLs without `.git` suffix
+- fixed: `git machete` now correctly displays only available commands (without help topics)
+- fixed: `git machete traverse -y -M` no longer exits with error
+- fixed: `git machete` commands no longer exit with stack trace when interrupted by Ctrl+C
+
 ## New in git-machete 3.13.0
 
 - improved: modify formatting of command line documentation
diff --git a/ci/checks/enforce-indent-two-spaces-outside-python.sh b/ci/checks/enforce-indent-two-spaces-outside-python.sh
index efc186d..3e8bfb3 100755
--- a/ci/checks/enforce-indent-two-spaces-outside-python.sh
+++ b/ci/checks/enforce-indent-two-spaces-outside-python.sh
@@ -3,7 +3,7 @@
 set -e -o pipefail -u
 
 self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-self_name=$(basename --suffix=.sh "$0")
+self_name=$(basename -s .sh "$0")
 
-git ls-files ':!*.awk' ':!/.circleci/config.yml' ':!*/Dockerfile' ':!docs/*' ':!graphics/setup-sandbox' ':!*.md' ':!*.png' ':!*.py' ':!*.svg' \
+git ls-files ':!*.awk' ':!/.circleci/config.yml' ':!*/Dockerfile' ':!docs/*' ':!*.gif' ':!graphics/setup-sandbox' ':!*.md' ':!*.png' ':!*.py' ':!*.svg' \
   | xargs awk -f "$self_dir/$self_name.awk"
diff --git a/ci/checks/enforce-release-notes-up-to-date.sh b/ci/checks/enforce-release-notes-up-to-date.sh
index 4277384..cedcd62 100755
--- a/ci/checks/enforce-release-notes-up-to-date.sh
+++ b/ci/checks/enforce-release-notes-up-to-date.sh
@@ -3,7 +3,7 @@
 set -e -o pipefail -u
 
 current_version=$(python3 setup.py --version)
-release_notes_version=$(sed '3!d' RELEASE_NOTES.md | egrep -o '(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)')
+release_notes_version=$(sed '3!d' RELEASE_NOTES.md | grep -Eo '(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)')
 
 if [[ $current_version != "$release_notes_version" ]]; then
   echo "Please update RELEASE_NOTES.md! Current version: $current_version, latest version in RELEASE_NOTES.md (line 3): $release_notes_version."
diff --git a/ci/checks/prohibit-strings-split-without-delimiter.awk b/ci/checks/prohibit-strings-split-without-delimiter.awk
index eb53533..72936fb 100755
--- a/ci/checks/prohibit-strings-split-without-delimiter.awk
+++ b/ci/checks/prohibit-strings-split-without-delimiter.awk
@@ -13,11 +13,11 @@ prev_is_unsafe_terminated_string && /^ *f?["'][^ ]/ {
   exit_code = 1
 }
 
-                 { prev_is_unsafe_terminated_string = 0 }
+                      { prev_is_unsafe_terminated_string = 0 }
 
-/[^ ]["'] *\\?$/ { prev_is_unsafe_terminated_string = 1 }
+/[^ ]["'] *\\?$/      { prev_is_unsafe_terminated_string = 1 }
 
-/\\n["'] *\\?$/  { prev_is_unsafe_terminated_string = 0 }
+/(\\n|\})["'] *\\?$/  { prev_is_unsafe_terminated_string = 0 }
 
 { prev = $0 }
 
diff --git a/ci/checks/prohibit-strings-split-without-delimiter.sh b/ci/checks/prohibit-strings-split-without-delimiter.sh
index 3d2bbf2..dd99735 100755
--- a/ci/checks/prohibit-strings-split-without-delimiter.sh
+++ b/ci/checks/prohibit-strings-split-without-delimiter.sh
@@ -3,7 +3,7 @@
 set -e -o pipefail -u
 
 self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-self_name=$(basename --suffix=.sh "$0")
+self_name=$(basename -s .sh "$0")
 
 git ls-files '*.py' ':!tests/**' \
   | xargs awk -f "$self_dir/$self_name.awk"
diff --git a/ci/checks/prohibit-trailing-whitespace.sh b/ci/checks/prohibit-trailing-whitespace.sh
index 2135518..4504d42 100755
--- a/ci/checks/prohibit-trailing-whitespace.sh
+++ b/ci/checks/prohibit-trailing-whitespace.sh
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-if git grep -EIn '\s+$' -- :!'*.py'; then
+if git grep -EIn ' +$' -- :!'*.py'; then
   echo 'The above lines contain trailing whitespace, please tidy up'
   exit 1
 fi
diff --git a/ci/nixpkgs-build/Dockerfile b/ci/nixpkgs-build/Dockerfile
index f113f24..3166eb6 100644
--- a/ci/nixpkgs-build/Dockerfile
+++ b/ci/nixpkgs-build/Dockerfile
@@ -8,4 +8,4 @@ RUN chmod +x /root/entrypoint.sh
 CMD /root/entrypoint.sh
 
 WORKDIR /root/nixpkgs
-ENV EXPRESSION_PATH=pkgs/applications/version-management/git-and-tools/git-machete/default.nix
+ENV EXPRESSION_PATH=pkgs/applications/version-management/git-machete/default.nix
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 8576e6e..86a8592 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.13.0'
+__version__ = '3.13.1'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 444fc10..b6f6624 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -49,7 +49,7 @@ command_groups: List[Tuple[str, List[str]]] = [
 commands_and_aliases = list(long_docs.keys()) + list(alias_by_command.keys())
 
 
-def get_help_description(command: str = None) -> str:
+def get_help_description(display_help_topics: bool, command: str = None) -> str:
     usage_str = ''
     if command and command in commands_and_aliases:
         if command in long_docs:
@@ -65,6 +65,9 @@ def get_help_description(command: str = None) -> str:
             "    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:
+            if not display_help_topics:
+                if hdr == 'General topics':
+                    cmds = [topic for topic in cmds if topic not in ['config', 'format', 'hooks']]
             usage_str += underline(hdr) + '\n\n'
             for cm in cmds:
                 alias = f", {alias_by_command[cm]}" if cm in alias_by_command else ""
@@ -114,7 +117,7 @@ class MacheteHelpAction(argparse.Action):
     ) -> 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))
+        print(get_help_description(display_help_topics=True, command=command_name))
         parser.exit(status=ExitCode.SUCCESS)
 
 
@@ -451,12 +454,13 @@ def update_cli_opts_using_parsed_args(
             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
+            cli_opts.opt_yes = 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
+    if not cli_opts.opt_merge:
+        if cli_opts.opt_yes or cli_opts.opt_n:
+            cli_opts.opt_no_interactive_rebase = True
 
 
 def update_cli_options_using_config_keys(
@@ -518,13 +522,13 @@ def launch(orig_args: List[str]) -> None:
         set_utils_global_variables(cli_opts)
 
         if not orig_args:
-            print(get_help_description())
+            print(get_help_description(display_help_topics=False))
             exit_script(ExitCode.ARGUMENT_ERROR)
 
         cmd = parsed_cli.command
 
         if cmd == "help":
-            print(get_help_description(parsed_cli.topic_or_cmd))
+            print(get_help_description(display_help_topics=True, command=parsed_cli.topic_or_cmd))
             exit_script(ExitCode.SUCCESS)
         elif cmd == "version":
             version()
diff --git a/git_machete/client.py b/git_machete/client.py
index 718f22b..60bd77b 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -979,17 +979,17 @@ class MacheteClient:
                             # the above call to 'fork_point_hash'.
                             fp_branches_formatted: str = " and ".join(
                                 sorted(underline(lb_or_rb) for lb, lb_or_rb in fork_point_branches_cached[branch]))
-                            fp_suffix: str = " %s %s %s seems to be a part of the unique history of %s" % \
-                                             (colored(utils.get_right_arrow(), AnsiEscapeCodes.RED),
-                                              colored("fork point ???", AnsiEscapeCodes.RED),
-                                              "this commit" if opt_list_commits_with_hashes else f"commit {commit.short_hash}",
-                                              fp_branches_formatted)
+                            right_arrow = colored(utils.get_right_arrow(), AnsiEscapeCodes.RED)
+                            fork_point = colored("fork point ???", AnsiEscapeCodes.RED)
+                            fp_suffix: str = f' {right_arrow} {fork_point} \
+                                {"this commit" if opt_list_commits_with_hashes else f"commit {commit.short_hash}"}\
+                                seems to be a part of the unique history of {fp_branches_formatted}'
                         else:
                             fp_suffix = ''
                         print_line_prefix(branch, utils.get_vertical_bar())
-                        out.write(" %s%s%s\n" % (
-                            f"{dim(commit.short_hash)}  " if opt_list_commits_with_hashes else "", dim(commit.subject),
-                            fp_suffix))
+                        out.write(f' {f"{dim(commit.short_hash)}  " if opt_list_commits_with_hashes else ""}'
+                                  f'{dim(commit.subject)}'
+                                  f'{fp_suffix}\n')
 
                 junction: str
                 if utils.ascii_only:
@@ -1025,7 +1025,7 @@ class MacheteClient:
                     prefix = "REVERTING "
                 else:
                     prefix = ""
-                current = "%s%s" % (bold(colored(prefix, AnsiEscapeCodes.RED)), bold(underline(branch, star_if_ascii_only=True)))
+                current = f"{bold(colored(prefix, AnsiEscapeCodes.RED))}{bold(underline(branch, star_if_ascii_only=True))}"
             else:
                 current = bold(branch)
 
@@ -1424,15 +1424,15 @@ class MacheteClient:
 
         result = [hash for (hash, gs) in branch_reflog if
                   hash not in hashes_to_exclude and not is_excluded_reflog_subject(hash, gs)]
+        reflog = (", ".join(result) or "<empty>")
         debug("computed filtered reflog (= reflog without branch creation "
-              "and branch reset events irrelevant for fork point/upstream inference): %s\n" % (", ".join(result) or "<empty>"))
+              f"and branch reset events irrelevant for fork point/upstream inference): {reflog}\n")
         return result
 
     def sync_annotations_to_github_prs(self) -> None:
-        remote, org, repo = self.__derive_remote_and_github_org_and_repo()
+        _, org, repo = self.__derive_remote_and_github_org_and_repo()
         current_user: Optional[str] = git_machete.github.derive_current_user_login()
         debug('Current GitHub user is ' + (current_user or '<none>'))
-        pr: GitHubPullRequest
         all_prs: List[GitHubPullRequest] = derive_pull_requests(org, repo)
         self.__sync_annotations_to_definition_file(all_prs, current_user)
 
@@ -1527,7 +1527,8 @@ class MacheteClient:
                     joined_branch_pairs = ", ".join(map(tupled(branch_pair_to_str), branch_pairs_))
                     yield dim(f"{hash_} => {joined_branch_pairs}")
 
-            debug("branches containing the given hash in their filtered reflog: \n%s\n" % "\n".join(log_result()))
+            branches = "\n".join(log_result())
+            debug(f"branches containing the given hash in their filtered reflog: \n{branches}\n")
 
         branch_full_hash: FullCommitHash = self.__git.get_commit_hash_by_revision(branch)
 
@@ -1883,7 +1884,11 @@ class MacheteClient:
             if verbose:
                 print(fmt(opt_yes_msg) if apply_fmt else opt_yes_msg)
             return 'y'
-        return input(fmt(msg) if apply_fmt else msg).lower()
+        try:
+            ans: str = input(fmt(msg) if apply_fmt else msg).lower()
+        except InterruptedError:
+            sys.exit(1)
+        return ans
 
     @staticmethod
     def pick(choices: List[LocalBranchShortName], name: str, apply_fmt: bool = True) -> LocalBranchShortName:
@@ -2134,7 +2139,7 @@ class MacheteClient:
                     'corresponds to GitHub (see `git remote -v` for details). \n'
                     'It is possible that you are using custom GitHub URL.\n'
                     'If that is the case, you can provide repository information explicitly, via these 3 git config keys: '
-                    '`machete.github.{remote,organization,repository}`\n'))
+                    '`machete.github.{{remote,organization,repository}}`\n'))
 
         if len(remote_and_github_org_and_repo) == 1:
             return remote_and_github_org_and_repo[list(remote_and_github_org_and_repo.keys())[0]]
@@ -2153,7 +2158,7 @@ class MacheteClient:
             f'Multiple non-origin remotes correspond to GitHub in this repository: '
             f'{", ".join(remote_and_github_org_and_repo.keys())} -> aborting. \n'
             f'You can also select the repository by providing 3 git config keys: '
-            '`machete.github.{remote,organization,repository}`\n')
+            f'`machete.github.{{remote,organization,repository}}`\n')
 
     def __get_remote_and_organization_and_repository_name_for_custom_url(self) -> RemoteAndOrganizationAndRepository:
         return RemoteAndOrganizationAndRepository(remote=self.__git.get_config_attr_or_none("machete.github.remote"),
@@ -2210,12 +2215,12 @@ class MacheteClient:
         milestone_path: str = self.__git.get_main_git_subpath('info', 'milestone')
         milestone: str = utils.slurp_file_or_empty(milestone_path).strip()
         if milestone:
-            print(fmt(f'Setting milestone of PR #{pr.number} to {milestone}...'), end='', flush=True)
+            print(fmt(f'Setting milestone of PR #{pr.number} to {milestone}... '), end='', flush=True)
             set_milestone_of_pull_request(org, repo, pr.number, milestone=milestone)
             print(fmt(ok_str))
 
         if current_user:
-            print(fmt(f'Adding `{current_user}` as assignee to PR #{pr.number}...'), end='', flush=True)
+            print(fmt(f'Adding `{current_user}` as assignee to PR #{pr.number}... '), end='', flush=True)
             add_assignees_to_pull_request(org, repo, pr.number, [current_user])
             print(fmt(ok_str))
 
@@ -2224,7 +2229,7 @@ class MacheteClient:
         if reviewers:
             print(
                 fmt(f'Adding {", ".join(f"`{reviewer}`" for reviewer in reviewers)} '
-                    f'as reviewer{"s" if len(reviewers) > 1 else ""} to PR #{pr.number}...'),
+                    f'as reviewer{"s" if len(reviewers) > 1 else ""} to PR #{pr.number}... '),
                 end='', flush=True)
             try:
                 add_reviewers_to_pull_request(org, repo, pr.number, reviewers)
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index e95824a..071d0a5 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -421,7 +421,7 @@ class GitContext:
         self.flush_caches()
 
     def __find_short_commit_hash_by_revision(self, revision: AnyRevision) -> ShortCommitHash:
-        return ShortCommitHash.of(self._popen_git("rev-parse", "--short", revision + "^{commit}").stdout.rstrip())
+        return ShortCommitHash.of(self._popen_git("rev-parse", "--short", revision + "^{commit}").stdout.rstrip())  # noqa: FS003
 
     def get_short_commit_hash_by_revision(self, revision: AnyRevision) -> ShortCommitHash:
         if revision not in self.__short_commit_hash_by_revision_cached:
@@ -432,7 +432,7 @@ class GitContext:
         # Without ^{commit}, 'git rev-parse --verify' will not only accept references to other kinds of objects (like trees and blobs),
         # but just echo the argument (and exit successfully) even if the argument doesn't match anything in the object store.
         try:
-            return FullCommitHash.of(self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{commit}").stdout.rstrip())
+            return FullCommitHash.of(self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{commit}").stdout.rstrip())  # noqa: FS003, E501
         except MacheteException:
             return None
 
@@ -447,7 +447,7 @@ class GitContext:
 
     def __find_tree_hash_by_revision(self, revision: AnyRevision) -> Optional[FullTreeHash]:
         try:
-            return FullTreeHash.of(self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{tree}").stdout.rstrip())
+            return FullTreeHash.of(self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{tree}").stdout.rstrip())  # noqa: FS003
         except MacheteException:
             return None
 
@@ -460,10 +460,10 @@ class GitContext:
 
     @staticmethod
     def is_full_hash(revision: AnyRevision) -> Optional[Match[str]]:
-        return re.match("^[0-9a-f]{40}$", revision)
+        return re.match("^[0-9a-f]{40}$", revision)  # noqa: FS003
 
     def is_commit_present_in_repository(self, revision: AnyRevision) -> bool:
-        return self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{commit}", allow_non_zero=True).exit_code == 0
+        return self._popen_git("rev-parse", "--verify", "--quiet", revision + "^{commit}", allow_non_zero=True).exit_code == 0  # noqa: FS003, E501
 
     def get_committer_unix_timestamp_by_revision(self, revision: AnyBranchName) -> int:
         if self.__committer_unix_timestamp_by_revision_cached is None:
@@ -978,7 +978,7 @@ class GitContext:
         # %gs - reflog subject
         output = self._popen_git("reflog", "show", "--format=%gd:%gs", "--date=raw").stdout
         for entry in utils.get_non_empty_lines(output):
-            pattern = "^HEAD@\\{([0-9]+) .+\\}:checkout: moving from (.+) to (.+)$"
+            pattern = "^HEAD@\\{([0-9]+) .+\\}:checkout: moving from (.+) to (.+)$"  # noqa: FS003
             match = re.search(pattern, entry)
             if match:
                 from_branch = match.group(2)
@@ -1010,7 +1010,7 @@ class GitContext:
             self, parent_revision: AnyRevision, msg: str, env: Dict[str, str]) -> FullCommitHash:
         # returns hash of the new commit
         return FullCommitHash.of(self._popen_git(
-            "commit-tree", "HEAD^{tree}", "-p", parent_revision, "-m", msg, env=env).stdout)
+            "commit-tree", "HEAD^{tree}", "-p", parent_revision, "-m", msg, env=env).stdout)  # noqa: FS003
 
     def delete_branch(self, branch_name: LocalBranchShortName, force: bool = False) -> int:
         self.flush_caches()
diff --git a/git_machete/github.py b/git_machete/github.py
index 433e11f..9898727 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -15,15 +15,16 @@ from git_machete.exceptions import MacheteException, UnprocessableEntityHTTPErro
 from git_machete.git_operations import GitContext, LocalBranchShortName
 
 GITHUB_TOKEN_ENV_VAR = 'GITHUB_TOKEN'
-# GitHub Enterprise deployments use alternate domains.
-# The logic in this module will need to be expanded to detect
-# and use alternate remote domains to provide enterprise support.
+
+# TODO (#137): Support GitHub Enterprise endpoints
 GITHUB_DOMAIN = "github.com"
+
+# GitHub DOES NOT allow trailing `.git` suffix in the repository name (also applies to multiple repetitions e.g. `repo_name.git.git`)
 GITHUB_REMOTE_PATTERNS = [
-    r"^https://.*@github\.com/(.*)/(.*)\.git$",
-    r"^https://github\.com/(.*)/(.*)\.git$",
-    r"^git@github\.com:(.*)/(.*)\.git$",
-    r"^ssh://git@github\.com/(.*)/(.*)\.git$"
+    r"^https://.*@github\.com/(.*)/(.*)$",
+    r"^https://github\.com/(.*)/(.*)$",
+    r"^git@github\.com:(.*)/(.*)$",
+    r"^ssh://git@github\.com/(.*)/(.*)$"
 ]
 
 
@@ -319,9 +320,11 @@ def get_parsed_github_remote_url(url: str, remote: str) -> Optional[RemoteAndOrg
     for pattern in GITHUB_REMOTE_PATTERNS:
         match = re.match(pattern, url)
         if match:
+            org = match.group(1)
+            repo = match.group(2)
             return RemoteAndOrganizationAndRepository(remote=remote,
-                                                      organization=match.group(1),
-                                                      repository=match.group(2))
+                                                      organization=org,
+                                                      repository=repo if repo[-4:] != '.git' else repo[:-4])
     return None
 
 
diff --git a/requirements.coverage.txt b/requirements.coverage.txt
index d197156..e447726 100644
--- a/requirements.coverage.txt
+++ b/requirements.coverage.txt
@@ -1,3 +1,3 @@
-coverage==6.5.0
+coverage==7.0.1
 pytest==7.2.0
 pytest-mock==3.10.0
diff --git a/requirements.mypy.txt b/requirements.mypy.txt
index bc2962f..3f62718 100644
--- a/requirements.mypy.txt
+++ b/requirements.mypy.txt
@@ -1,2 +1,2 @@
-mypy==0.982
+mypy==0.991
 pytest==7.2.0
diff --git a/requirements.pep8.txt b/requirements.pep8.txt
index 6f47bc9..72e2b50 100644
--- a/requirements.pep8.txt
+++ b/requirements.pep8.txt
@@ -1,1 +1,2 @@
-flake8==5.0.4
+flake8==6.0.0
+flake8-use-fstring==1.4
diff --git a/requirements.sphinx-docs.txt b/requirements.sphinx-docs.txt
index 2c673d1..2cc8302 100644
--- a/requirements.sphinx-docs.txt
+++ b/requirements.sphinx-docs.txt
@@ -1,2 +1,2 @@
 sphinx==5.3.0
-sphinx-rtd-theme==1.1.0
+sphinx-rtd-theme==1.1.1
diff --git a/tox.ini b/tox.ini
index 42cb94c..0aee203 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,7 +24,7 @@ deps =
 [testenv:pep8]
 deps =
   -r{toxinidir}/requirements.pep8.txt
-commands = flake8
+commands = flake8 --enable-extensions=FS003
 
 # Set flake8 configuration options which are used by the `flake8` command in [testenv:pep8]
 [flake8]


Test patch:
diff --git a/requirements.testenv.txt b/requirements.testenv.txt
index b2f90aa..ff6ef19 100644
--- a/requirements.testenv.txt
+++ b/requirements.testenv.txt
@@ -1,3 +1,3 @@
 pytest==7.2.0
 pytest-mock==3.10.0
-pytest-xdist==3.0.2
+pytest-xdist==3.1.0
diff --git a/tests/mockers.py b/tests/mockers.py
index 42436ec..3c2becb 100644
--- a/tests/mockers.py
+++ b/tests/mockers.py
@@ -78,7 +78,7 @@ class GitRepositorySandbox:
         return self
 
     def commit(self, message: str = "Some commit message.") -> "GitRepositorySandbox":
-        f = "%s.txt" % "".join(random.choice(string.ascii_letters) for _ in range(20))
+        f = f'{"".join(random.choice(string.ascii_letters) for _ in range(20))}.txt'
         self.execute(f"touch {f}")
         self.execute(f"git add {f}")
         self.execute(f'git commit -m "{message}"')
diff --git a/tests/test_git_operations.py b/tests/test_git_operations.py
index eb777bb..80a3375 100644
--- a/tests/test_git_operations.py
+++ b/tests/test_git_operations.py
@@ -32,7 +32,7 @@ class TestGitOperations:
         master_branch_first_commit_hash = get_current_commit_hash()
 
         git = GitContext()
-        assert git._run_git("rev-parse", "--verify", "--quiet", master_branch_first_commit_hash + "^{commit}", allow_non_zero=True) == 0
+        assert git._run_git("rev-parse", "--verify", "--quiet", master_branch_first_commit_hash + "^{commit}", allow_non_zero=True) == 0  # noqa: FS003, E501
         assert git._run_git("rev-parse", "HEAD") == 0
 
     def test_popen_git(self) -> None:
diff --git a/tests/test_github.py b/tests/test_github.py
index ad70d75..bbfe7a7 100644
--- a/tests/test_github.py
+++ b/tests/test_github.py
@@ -8,7 +8,7 @@ import pytest
 
 from git_machete.exceptions import MacheteException
 from git_machete.git_operations import LocalBranchShortName
-from git_machete.github import get_parsed_github_remote_url
+from git_machete.github import get_parsed_github_remote_url, RemoteAndOrganizationAndRepository
 from git_machete.options import CommandLineOptions
 
 from .mockers import (GitRepositorySandbox, MockContextManager,
@@ -193,7 +193,7 @@ class TestGithub:
         launch_command("discover", "-y")
         expected_error_message = (
             "Multiple non-origin remotes correspond to GitHub in this repository: origin_1, origin_2 -> aborting. \n"
-            "You can also select the repository by providing 3 git config keys: `machete.github.{remote,organization,repository}`\n"
+            "You can also select the repository by providing 3 git config keys: `machete.github.{remote,organization,repository}`\n"  # noqa: FS003, E501
         )
         with pytest.raises(MacheteException) as e:
             launch_command("github", "retarget-pr")
@@ -328,7 +328,7 @@ class TestGithub:
                 .sleep(1)
                 .commit_amend("Ignore trailing data (amended)")
                 .push()
-                .reset_to("ignore-trailing@{1}")
+                .reset_to("ignore-trailing@{1}")  # noqa: FS003
                 .delete_branch("root")
                 .add_remote('new_origin', 'https://github.com/user/repo.git')
         )
@@ -451,7 +451,7 @@ class TestGithub:
                 .sleep(1)
                 .commit_amend("Ignore trailing data (amended)")
                 .push()
-                .reset_to("ignore-trailing@{1}")
+                .reset_to("ignore-trailing@{1}")  # noqa: FS003
                 .delete_branch("root")
                 .new_branch('chore/fields')
                 .commit("remove outdated fields")
@@ -1430,3 +1430,17 @@ class TestGithub:
 
         with pytest.raises(subprocess.CalledProcessError):
             self.repo_sandbox.check_out("mars")
+
+    def test_github_remote_patterns(self) -> None:
+        organization = 'virtuslab'
+        repository = 'repo_sandbox'
+        urls = [f'https://tester@github.com/{organization}/{repository}',
+                f'https://github.com/{organization}/{repository}',
+                f'git@github.com:{organization}/{repository}',
+                f'ssh://git@github.com/{organization}/{repository}']
+        urls = urls + [url + '.git' for url in urls]
+
+        for url in urls:
+            remote_and_organization_and_repository: RemoteAndOrganizationAndRepository = get_parsed_github_remote_url(url, 'origin')
+            assert remote_and_organization_and_repository.organization == organization
+            assert remote_and_organization_and_repository.repository == repository
diff --git a/tests/test_show.py b/tests/test_show.py
index d352536..0f9e328 100644
--- a/tests/test_show.py
+++ b/tests/test_show.py
@@ -55,7 +55,7 @@ class TestShow:
             .sleep(1)
             .commit_amend("Ignore trailing data (amended)")
             .push()
-            .reset_to("ignore-trailing@{1}")
+            .reset_to("ignore-trailing@{1}")  # noqa: FS003
             .delete_branch("root")
         )
 
diff --git a/tests/test_traverse.py b/tests/test_traverse.py
index d08960b..ec5ee88 100644
--- a/tests/test_traverse.py
+++ b/tests/test_traverse.py
@@ -56,7 +56,7 @@ class TestTraverse:
             .sleep(1)
             .commit_amend("Ignore trailing data (amended)")
             .push()
-            .reset_to("ignore-trailing@{1}")
+            .reset_to("ignore-trailing@{1}")  # noqa: FS003
             .delete_branch("root")
         )
 
@@ -319,3 +319,32 @@ class TestTraverse:
               o-ignore-trailing
             """,
         )
+
+    def test_traverse_with_merge(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        (
+            self.repo_sandbox
+            .new_branch("develop")
+            .commit("develop commit 1")
+            .new_branch('mars')
+            .commit('mars commit 1')
+            .new_branch('snickers')
+            .commit('snickers commit')
+            .check_out('mars')
+            .commit('mars commit 2')
+            .check_out('develop')
+            .commit('develop commit 2')
+        )
+
+        launch_command("discover", "-y", "--roots=develop")
+        launch_command("traverse", '-y', '-M', '-n')
+        assert_command(
+            ["status"],
+            """
+            develop
+            |
+            o-mars
+              |
+              o-snickers *
+            """
+        )