Repository: VirtusLab/git-machete
Pull request #1259 (2024-05-24 13:09:59)

Release v3.26.0


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 17db617..6809bee 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,10 @@
 # Release notes
 
+## New in git-machete 3.26.0
+
+- added: better detection of squash merges and rebases, controlled by flag `--squash-merge-detection={none,simple,exact}` (`status` and `traverse`) and git config key `machete.squashMergeDetection` (contributed by @gjulianm)
+- deprecated: `--no-detect-squash-merges` flag in `status` and `traverse` &mdash; use `--squash-merge-detection=none` instead (contributed by @gjulianm)
+
 ## New in git-machete 3.25.3
 
 - fixed: `-y` option in `git machete traverse` automatically sets `--no-edit-merge` flag, to retain behavior when the `update=merge` qualifier is set (contributed by @gjulianm)
diff --git a/docs/generate-sphinx-man.sh b/docs/generate-sphinx-man.sh
index 9c7e5bc..1b9949c 100755
--- a/docs/generate-sphinx-man.sh
+++ b/docs/generate-sphinx-man.sh
@@ -6,6 +6,3 @@ target_dir=$1
 
 # `-t man` is needed for the conditionally rendered sections
 sphinx-build -W --keep-going -b man -t man docs/source "$target_dir"
-
-# To view the generated file, use
-#   groff -man -Tascii < docs/man/git-machete.1 | less -r
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 204112e..73805f9 100644
--- a/docs/man/git-machete.1
+++ b/docs/man/git-machete.1
@@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "GIT-MACHETE" "1" "May 08, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "May 24, 2024" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.25.3
+git-machete \- git-machete 3.26.0
 .sp
 git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
 .sp
@@ -41,15 +41,11 @@ Using this tool, you can maintain \fBsmall, focused, easy\-to\-review pull reque
 .sp
 A look at a \fBgit machete status\fP gives an instant answer to the questions:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 What branches are in this repository?
 .IP \(bu 2
 What is going to be merged (rebased/pushed/pulled) and to what?
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 \fBgit machete traverse\fP semi\-automatically traverses the branches, helping you effortlessly rebase, merge, push and pull.
 .sp
@@ -117,6 +113,20 @@ git machete commands and help topics:
 .IP \(bu 2
 \fI\%version\fP          \-\- Display the version and exit
 .UNINDENT
+.INDENT 0.0
+.TP
+.B  \-\-debug
+Log detailed diagnostic info, including outputs of the executed git commands.
+.TP
+.B  \-h\fP,\fB  \-\-help
+Print help and exit.
+.TP
+.B  \-v\fP,\fB  \-\-verbose
+Log the executed git commands.
+.TP
+.B  \-\-version
+Print version and exit.
+.UNINDENT
 .SH ADD
 .sp
 \fBUsage:\fP
@@ -134,16 +144,12 @@ git machete add [\-o|\-\-onto=<target\-upstream\-branch>] [\-R|\-\-as\-root] [\-
 Adds the provided <branch> (or the current branch, if none specified) to the branch layout file.
 If <branch> is provided but no local branch with the given name exists:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 if a remote branch of the same name exists in exactly one remote,
 then user is asked whether to check out this branch locally (as in \fBgit checkout\fP),
 .IP \(bu 2
 otherwise, user is asked whether it should be created as a new local branch.
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 If the branch layout file is empty or \fB\-R/\-\-as\-root\fP is provided, the branch will be added as a root of the tree of branch dependencies.
 Otherwise, the desired upstream (parent) branch can be specified with \fB\-o/\-\-onto\fP\&.
@@ -189,8 +195,6 @@ and subsequently slides out \fBD\fP\&. All three steps require manual confirmati
 .sp
 The downstream \fBD\fP is selected according to the following criteria:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 if \fBC\fP has exactly one downstream (child) branch \fBd\fP connected with a green edge (see help for \fI\%status\fP) to \fBC\fP
 or is overridden, then \fBd\fP is selected as \fBD\fP,
@@ -201,8 +205,6 @@ if \fBC\fP has more than one downstream branch connected with a green edge to \f
 then user is asked to pick the branch to fast\-forward merge into (similarly to what happens in \fBgit machete go down\fP).
 If \fB\-\-yes\fP is specified, then \fBadvance\fP fails.
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 As an example, if \fBgit machete status \-\-color=never \-\-list\-commits\fP is as follows:
 .INDENT 0.0
@@ -410,6 +412,28 @@ The override data is stored under \fBmachete.overrideForkPoint.<branch>.to\fP gi
 There should be \fBno\fP need for the user to interact with this key directly,
 \fBgit machete fork\-point\fP with flags should be used instead.
 .TP
+.B \fBmachete.squashMergeDetection\fP:
+Controls the algorithm used to detect squash merges. Possible values are:
+.INDENT 7.0
+.IP \(bu 2
+\fBnone\fP: Fastest mode, with no squash merge/rebase detection. Only \fIstrict\fP (fast\-forward or 2\-parent) merges are detected.
+.IP \(bu 2
+\fBsimple\fP (default): Compares the tree (files & directories in the commit) of the downstream branch with the trees of the upstream branch.
+This detects squash merges/rebases as long as there exists a squash/rebase commit in the upstream that has the identical tree to what\(aqs in the downstream branch.
+.IP \(bu 2
+\fBexact\fP: Compares the patch (diff introduced by the commits) of the downstream branch with the patches of the upstream branch.
+This detects squash merges in more cases than \fBsimple\fP mode.
+However, it might have a significant performance impact on large repositories as it requires computing patches for commits in the upstream branch.
+.UNINDENT
+.sp
+This has an impact on:
+.INDENT 7.0
+.IP \(bu 2
+whether a grey edge is displayed in \fBstatus\fP,
+.IP \(bu 2
+whether \fBtraverse\fP suggests to slide out the branch.
+.UNINDENT
+.TP
 .B \fBmachete.status.extraSpaceBeforeBranchName\fP:
 To make it easier to select branch name from the \fBstatus\fP output on certain terminals
 (like \fI\%Alacritty\fP), you can add an extra space between └─ and \fBbranch name\fP
@@ -474,6 +498,9 @@ Example: \fBGIT_MACHETE_REBASE_OPTS=\(dq\-\-keep\-empty \-\-rebase\-merges\(dq g
 .TP
 .B \fBGITHUB_TOKEN\fP
 Used to store GitHub API token. Used by commands such as \fBanno \-\-sync\-github\-prs\fP and \fBgithub\fP\&.
+.TP
+.B \fBGITLAB_TOKEN\fP
+Used to store GitLab API token. Used by commands such as \fBanno \-\-sync\-gitlab\-prs\fP and \fBgitlab\fP\&.
 .UNINDENT
 .SH CLEAN
 .sp
@@ -707,8 +734,6 @@ Opens an editor and lets you edit the branch layout file manually.
 .sp
 The editor is determined by checking up the following locations:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 \fB$GIT_MACHETE_EDITOR\fP
 .IP \(bu 2
@@ -726,8 +751,6 @@ The editor is determined by checking up the following locations:
 .IP \(bu 2
 \fBvi\fP
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 and selecting the first one that is defined and points to an executable file accessible on \fBPATH\fP\&.
 .sp
@@ -762,8 +785,6 @@ The file is always called \fBmachete\fP and is located in the git directory of t
 .sp
 Three cases are possible:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 if \fBgit machete\fP is executed from a regular working directory (not a worktree or submodule),
 the file is located under \fB\&.git/machete\fP,
@@ -779,8 +800,6 @@ if \fBmachete.worktree.useTopLevelMacheteFile\fP is false, the file is located u
 .IP \(bu 2
 if \fBgit machete\fP is executed from a \fBsubmodule\fP, this file is located in the git folder of the submodule itself under \fB\&.git/modules/.../machete\fP\&.
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .SH FORK-POINT
 .sp
 \fBUsage:\fP
@@ -806,8 +825,6 @@ Fork point of the given \fB<branch>\fP is the commit at which the history of the
 Fork point is assumed by many \fBgit machete\fP commands as the place where the unique history of the \fB<branch>\fP starts.
 The range of commits between the fork point and the tip of the given branch is, for instance:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 listed for each branch by \fBgit machete status \-\-list\-commits\fP
 .IP \(bu 2
@@ -815,8 +832,6 @@ passed to \fBgit rebase\fP by \fBgit machete\fP \fBreapply\fP/\fBslide\-out\fP/\
 .IP \(bu 2
 provided to \fBgit diff\fP/\fBlog\fP by \fBgit machete\fP \fBdiff\fP/\fBlog\fP\&.
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 \fBgit machete\fP assumes fork point of \fB<branch>\fP is the most recent commit in the log of \fB<branch>\fP that has NOT been introduced on that very branch,
 but instead occurs on a reflog (see help for \fBgit reflog\fP) of some other branch.
@@ -1346,8 +1361,6 @@ where <direction> is one of: \fBd[own]\fP, \fBf[irst]\fP, \fBl[ast]\fP, \fBn[ext
 .sp
 Checks out the branch specified by the given direction relative to the current branch:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 \fBdown\fP:    the direct children/downstream branch of the current branch.
 .IP \(bu 2
@@ -1367,8 +1380,6 @@ since all branches are usually meant to be ultimately merged to one of those.
 .IP \(bu 2
 \fBup\fP:      the direct parent/upstream branch of the current branch.
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 Roughly equivalent to \fBgit checkout $(git machete show <direction>)\fP\&.
 .SH HELP
@@ -1402,8 +1413,6 @@ is slid out by \fBadvance\fP, \fBslide\-out\fP or \fBtraverse\fP\&.
 .sp
 At least two parameters (branch names) are passed to the hook:
 .INDENT 7.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 <new\-upstream> is the upstream of the branch that has been slid out, or in case of multiple branches being slid out
 \-\-\- the upstream of the highest slid out branch;
@@ -1413,15 +1422,11 @@ At least two parameters (branch names) are passed to the hook:
 <new\-downstreams> are all the following (possibly zero) parameters, which correspond to all original downstreams
 of <lowest\-slid\-out\-branch>, now reattached as the downstreams of <new\-upstream>.
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 Note that this may be zero, one, or multiple branches.
 .sp
 Note: the hook, if present, is executed:
 .INDENT 7.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 zero or once during a \fBadvance\fP execution (depending on whether the slide\-out has been confirmed or not),
 .IP \(bu 2
@@ -1429,8 +1434,6 @@ exactly once during a \fBslide\-out\fP execution (even if multiple branches are
 .IP \(bu 2
 zero or more times during \fBtraverse\fP (every time a slide\-out operation is confirmed).
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 If the hook returns a non\-zero exit code, then an error is raised and the execution of the command is aborted \-\-\-
 \fBslide\-out\fP won\(aqt attempt rebase of the new downstream branches and \fBtraverse\fP won\(aqt continue the traversal.
@@ -1492,8 +1495,6 @@ Returns with zero exit code if the given branch (or current branch, if none spec
 .sp
 Returns with a non\-zero exit code in case:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 the <branch> is provided but isn\(aqt managed (or doesn\(aqt exist), or
 .IP \(bu 2
@@ -1501,8 +1502,6 @@ the <branch> isn\(aqt provided and the current branch isn\(aqt managed, or
 .IP \(bu 2
 the <branch> isn\(aqt provided and there\(aqs no current branch (detached HEAD).
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .SH LIST
 .sp
 \fBUsage:\fP
@@ -1521,8 +1520,6 @@ where <category> is one of: \fBaddable\fP, \fBchildless\fP, \fBmanaged\fP, \fBsl
 .sp
 Lists all branches that fall into one of the specified categories:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 \fBaddable\fP: all branches (local or remote) than can be added to the branch layout file,
 .IP \(bu 2
@@ -1540,8 +1537,6 @@ Lists all branches that fall into one of the specified categories:
 \fBwith\-overridden\-fork\-point\fP: all local branches that have a \fI\%fork point\fP override set up
 (even if this override does not affect the location of their fork point anymore).
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 This command is generally not meant for a day\-to\-day use, it\(aqs mostly needed for the sake of branch name completion in shell.
 .SH LOG
@@ -1619,8 +1614,6 @@ displayed relative to given <branch>, or the current checked out branch if <bran
 .sp
 Outputs name of the branch (or possibly multiple branches, in case of \fBdown\fP) that is:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 \fBcurrent\fP: the current branch; exits with a non\-zero status if none (detached HEAD)
 .IP \(bu 2
@@ -1642,8 +1635,6 @@ since all branches are usually meant to be ultimately merged to one of those.
 .IP \(bu 2
 \fBup\fP:      the direct parent/upstream branch of the given branch.
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .SH SLIDE-OUT
 .sp
 \fBUsage:\fP
@@ -1653,7 +1644,9 @@ since all branches are usually meant to be ultimately merged to one of those.
 .nf
 .ft C
 git machete slide\-out \-\-removed\-from\-remote [\-\-delete]
-git machete slide\-out [\-d|\-\-down\-fork\-point=<down\-fork\-point\-commit>] [\-\-delete] [\-M|\-\-merge] [\-n|\-\-no\-edit\-merge|\-\-no\-interactive\-rebase] [<branch> [<branch> [<branch> ...]]]
+git machete slide\-out [\-d|\-\-down\-fork\-point=<down\-fork\-point\-commit>] [\-\-delete]
+                      [\-M|\-\-merge] [\-n|\-\-no\-edit\-merge|\-\-no\-interactive\-rebase]
+                      [<branch> [<branch> [<branch> ...]]]
 .ft P
 .fi
 .UNINDENT
@@ -1752,8 +1745,6 @@ Not allowed if updating by merge.
 Slide out managed branches whose remote tracking branches have been deleted and that have no downstreams.
 In other words, this deletes all branches except:
 .INDENT 7.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 those that are unmanaged,
 .IP \(bu 2
@@ -1764,8 +1755,6 @@ those whose remote tracking branches still exist (not deleted remotely),
 those that have at least one downstream (child) branch.
 .UNINDENT
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 \fBEnvironment variables:\fP
 .INDENT 0.0
@@ -1814,7 +1803,9 @@ Specifies the alternative fork point commit after which the squashed part of his
 .sp
 .nf
 .ft C
-git machete s[tatus] [\-\-color=WHEN] [\-l|\-\-list\-commits] [\-L|\-\-list\-commits\-with\-hashes] [\-\-no\-detect\-squash\-merges]
+git machete s[tatus] [\-\-color=WHEN]
+                     [\-l|\-\-list\-commits] [\-L|\-\-list\-commits\-with\-hashes]
+                     [\-\-squash\-merge\-detection=MODE]
 .ft P
 .fi
 .UNINDENT
@@ -1824,8 +1815,6 @@ Displays a tree\-shaped status of the branches listed in the branch layout file.
 .sp
 Apart from simply ASCII\-formatting the branch layout file, this also:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 colors the edges between upstream (parent) and downstream (children) branches:
 .INDENT 2.0
@@ -1852,8 +1841,6 @@ displays the output of \fBmachete\-status\-branch hook\fP (see help for \fI\%hoo
 .IP \(bu 2
 optionally lists commits introduced on each branch if \fB\-l/\-\-list\-commits\fP or \fB\-L/\-\-list\-commits\-with\-hashes\fP is supplied.
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 Name of the currently checked\-out branch is underlined (or shown in blue on terminals that don\(aqt support underline).
 .sp
@@ -1935,13 +1922,41 @@ Additionally list the commits introduced on each branch.
 Additionally list the short hashes and messages of commits introduced on each branch.
 .TP
 .B  \-\-no\-detect\-squash\-merges
+\fBDeprecated\fP, use \fB\-\-squash\-merge\-detection=none\fP instead.
 Only consider \fIstrict\fP (fast\-forward or 2\-parent) merges, rather than rebase/squash merges,
 when detecting if a branch is merged into its upstream (parent).
+.TP
+.BI \-\-squash\-merge\-detection\fB= MODE
+Specify the mode for detection of rebase/squash merges (grey edges).
+\fBMODE\fP can be \fBnone\fP (fastest, no squash merges are detected), \fBsimple\fP (default) or \fBexact\fP (slowest).
+See the below paragraph on \fBmachete.squashMergeDetection\fP git config key for more details.
 .UNINDENT
 .sp
 \fBGit config keys:\fP
 .INDENT 0.0
 .TP
+.B \fBmachete.squashMergeDetection\fP:
+Controls the algorithm used to detect squash merges. Possible values are:
+.INDENT 7.0
+.IP \(bu 2
+\fBnone\fP: Fastest mode, with no squash merge/rebase detection. Only \fIstrict\fP (fast\-forward or 2\-parent) merges are detected.
+.IP \(bu 2
+\fBsimple\fP (default): Compares the tree (files & directories in the commit) of the downstream branch with the trees of the upstream branch.
+This detects squash merges/rebases as long as there exists a squash/rebase commit in the upstream that has the identical tree to what\(aqs in the downstream branch.
+.IP \(bu 2
+\fBexact\fP: Compares the patch (diff introduced by the commits) of the downstream branch with the patches of the upstream branch.
+This detects squash merges in more cases than \fBsimple\fP mode.
+However, it might have a significant performance impact on large repositories as it requires computing patches for commits in the upstream branch.
+.UNINDENT
+.sp
+This has an impact on:
+.INDENT 7.0
+.IP \(bu 2
+whether a grey edge is displayed in \fBstatus\fP,
+.IP \(bu 2
+whether \fBtraverse\fP suggests to slide out the branch.
+.UNINDENT
+.TP
 .B \fBmachete.status.extraSpaceBeforeBranchName\fP
 To make it easier to select branch name from the \fBstatus\fP output on certain terminals
 (like \fI\%Alacritty\fP), you can add an extra space between └─ and \fBbranch name\fP
@@ -1956,9 +1971,9 @@ by setting \fBgit config machete.status.extraSpaceBeforeBranchName true\fP\&.
 .nf
 .ft C
 git machete t[raverse] [\-F|\-\-fetch] [\-l|\-\-list\-commits] [\-M|\-\-merge]
-                       [\-n|\-\-no\-edit\-merge|\-\-no\-interactive\-rebase] [\-\-no\-detect\-squash\-merges]
-                       [\-\-[no\-]push] [\-\-[no\-]push\-untracked]
-                       [\-\-return\-to=WHERE] [\-\-start\-from=WHERE] [\-w|\-\-whole] [\-W] [\-y|\-\-yes]
+                       [\-n|\-\-no\-edit\-merge|\-\-no\-interactive\-rebase] [\-\-[no\-]push] [\-\-[no\-]push\-untracked]
+                       [\-\-return\-to=WHERE] [\-\-start\-from=WHERE] [\-\-squash\-merge\-detection=MODE]
+                       [\-w|\-\-whole] [\-W] [\-y|\-\-yes]
 .ft P
 .fi
 .UNINDENT
@@ -1970,8 +1985,6 @@ This behavior can, however, be customized using options: \fB\-\-start\-from=\fP,
 .sp
 For each branch, the command:
 .INDENT 0.0
-.INDENT 3.5
-.INDENT 0.0
 .IP \(bu 2
 detects if the branch is merged (grey edge) to its parent (aka upstream):
 .INDENT 2.0
@@ -2013,8 +2026,6 @@ and finally, if any of the above operations has been successfully completed:
 prints the updated \fBstatus\fP\&.
 .UNINDENT
 .UNINDENT
-.UNINDENT
-.UNINDENT
 .sp
 By default \fBtraverse\fP asks if the branch should be pushed. This behavior can, however, be changed with the \fBmachete.traverse.push\fP configuration key.
 It can also be customized using options: \fB\-\-[no\-]push\fP or \fB\-\-[no\-]push\-untracked\fP \-\-\- the order of the flags defines their precedence over each other
@@ -2023,7 +2034,7 @@ It can also be customized using options: \fB\-\-[no\-]push\fP or \fB\-\-[no\-]pu
 If the traverse flow is stopped (typically due to merge/rebase conflicts), just run \fBgit machete traverse\fP after the merge/rebase is finished.
 It will pick up the walk from the current branch.
 Unlike with \fBgit rebase\fP or \fBgit cherry\-pick\fP, there is no special \fB\-\-continue\fP flag, as \fBtraverse\fP is stateless.
-\fBtraverse\fP doesn\(aqt keep a state of its own like \fBgit rebase\fP does in \fB\&.git/rebase\-apply/\fP\&.
+\fBtraverse\fP does \fBnot\fP keep a state of its own like \fBgit rebase\fP does in \fB\&.git/rebase\-apply/\fP\&.
 .sp
 The rebase, push and slide\-out behaviors of \fBtraverse\fP can also be customized for each branch separately using \fIbranch qualifiers\fP\&.
 There are \fBpush=no\fP, \fBrebase=no\fP and \fBslide\-out=no\fP qualifiers that can be used to opt out of default behavior (rebasing, pushing and sliding the branch out).
@@ -2065,6 +2076,7 @@ Update by merge rather than by rebase.
 If updating by rebase, equivalent to \fB\-\-no\-interactive\-rebase\fP\&. If updating by merge, equivalent to \fB\-\-no\-edit\-merge\fP\&.
 .TP
 .B  \-\-no\-detect\-squash\-merges
+\fBDeprecated\fP, use \fB\-\-squash\-merge\-detection=none\fP instead.
 Only consider \fIstrict\fP (fast\-forward or 2\-parent) merges, rather than rebase/squash merges,
 when detecting if a branch is merged into its upstream (parent).
 .TP
@@ -2097,6 +2109,11 @@ WHERE can be \fBhere\fP (the current branch at the moment when traversal starts)
 \fBstay\fP (the default \-\-\- just stay wherever the traversal stops). Note: when user quits by \fBq\fP/\fByq\fP
 or when traversal is stopped because one of git actions fails, the behavior is always \fBstay\fP\&.
 .TP
+.BI \-\-squash\-merge\-detection\fB= MODE
+Specifies the mode for detection of rebase/squash merges (grey edges).
+\fBMODE\fP can be \fBnone\fP (fastest, no squash merges are detected), \fBsimple\fP (default) or \fBexact\fP (slowest).
+See the below paragraph on \fBmachete.squashMergeDetection\fP git config key for more details.
+.TP
 .BI \-\-start\-from\fB= WHERE
 Specifies the branch to start the traversal from; WHERE can be \fBhere\fP
 (the default \-\-\- current branch, must be managed by git machete), \fBroot\fP (root branch of the current branch,
@@ -2125,6 +2142,28 @@ Example: \fBGIT_MACHETE_REBASE_OPTS=\(dq\-\-keep\-empty \-\-rebase\-merges\(dq g
 \fBGit config keys:\fP
 .INDENT 0.0
 .TP
+.B \fBmachete.squashMergeDetection\fP:
+Controls the algorithm used to detect squash merges. Possible values are:
+.INDENT 7.0
+.IP \(bu 2
+\fBnone\fP: Fastest mode, with no squash merge/rebase detection. Only \fIstrict\fP (fast\-forward or 2\-parent) merges are detected.
+.IP \(bu 2
+\fBsimple\fP (default): Compares the tree (files & directories in the commit) of the downstream branch with the trees of the upstream branch.
+This detects squash merges/rebases as long as there exists a squash/rebase commit in the upstream that has the identical tree to what\(aqs in the downstream branch.
+.IP \(bu 2
+\fBexact\fP: Compares the patch (diff introduced by the commits) of the downstream branch with the patches of the upstream branch.
+This detects squash merges in more cases than \fBsimple\fP mode.
+However, it might have a significant performance impact on large repositories as it requires computing patches for commits in the upstream branch.
+.UNINDENT
+.sp
+This has an impact on:
+.INDENT 7.0
+.IP \(bu 2
+whether a grey edge is displayed in \fBstatus\fP,
+.IP \(bu 2
+whether \fBtraverse\fP suggests to slide out the branch.
+.UNINDENT
+.TP
 .B \fBmachete.traverse.push\fP
 To change the behavior of \fBgit machete traverse\fP command so that it doesn\(aqt push branches by default,
 you need to set config key \fBgit config machete.traverse.push false\fP\&.
@@ -2139,7 +2178,7 @@ Configuration key value can be overridden by the presence of the \fB\-\-push\fP
 .sp
 .nf
 .ft C
-git machete update [\-f|\-\-fork\-point=] [\-M|\-\-merge] [\-n|\-\-no\-edit\-merge|\-\-no\-interactive\-rebase]
+git machete update [\-f|\-\-fork\-point=<fork\-point\-commit>] [\-M|\-\-merge] [\-n|\-\-no\-edit\-merge|\-\-no\-interactive\-rebase]
 .ft P
 .fi
 .UNINDENT
diff --git a/docs/source/cli/add.rst b/docs/source/cli/add.rst
index 268f1a5..8831420 100644
--- a/docs/source/cli/add.rst
+++ b/docs/source/cli/add.rst
@@ -11,9 +11,9 @@ add
 Adds the provided <branch> (or the current branch, if none specified) to the branch layout file.
 If <branch> is provided but no local branch with the given name exists:
 
-    * if a remote branch of the same name exists in exactly one remote,
-      then user is asked whether to check out this branch locally (as in ``git checkout``),
-    * otherwise, user is asked whether it should be created as a new local branch.
+* if a remote branch of the same name exists in exactly one remote,
+  then user is asked whether to check out this branch locally (as in ``git checkout``),
+* otherwise, user is asked whether it should be created as a new local branch.
 
 If the branch layout file is empty or ``-R/--as-root`` is provided, the branch will be added as a root of the tree of branch dependencies.
 Otherwise, the desired upstream (parent) branch can be specified with ``-o/--onto``.
diff --git a/docs/source/cli/advance.rst b/docs/source/cli/advance.rst
index 7e4a99e..c417967 100644
--- a/docs/source/cli/advance.rst
+++ b/docs/source/cli/advance.rst
@@ -19,12 +19,12 @@ and subsequently slides out ``D``. All three steps require manual confirmation u
 
 The downstream ``D`` is selected according to the following criteria:
 
-    * if ``C`` has exactly one downstream (child) branch ``d`` connected with a :green:`green edge` (see help for :ref:`status`) to ``C``
-      or is overridden, then ``d`` is selected as ``D``,
-    * if ``C`` has no downstream branches connected with a :green:`green edge` to ``C``, then ``advance`` fails,
-    * if ``C`` has more than one downstream branch connected with a :green:`green edge` to ``C``,
-      then user is asked to pick the branch to fast-forward merge into (similarly to what happens in ``git machete go down``).
-      If ``--yes`` is specified, then ``advance`` fails.
+* if ``C`` has exactly one downstream (child) branch ``d`` connected with a :green:`green edge` (see help for :ref:`status`) to ``C``
+  or is overridden, then ``d`` is selected as ``D``,
+* if ``C`` has no downstream branches connected with a :green:`green edge` to ``C``, then ``advance`` fails,
+* if ``C`` has more than one downstream branch connected with a :green:`green edge` to ``C``,
+  then user is asked to pick the branch to fast-forward merge into (similarly to what happens in ``git machete go down``).
+  If ``--yes`` is specified, then ``advance`` fails.
 
 As an example, if ``git machete status --color=never --list-commits`` is as follows:
 
diff --git a/docs/source/cli/config.rst b/docs/source/cli/config.rst
index 3e546d8..6c3e93f 100644
--- a/docs/source/cli/config.rst
+++ b/docs/source/cli/config.rst
@@ -36,6 +36,9 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
     There should be **no** need for the user to interact with this key directly,
     ``git machete fork-point`` with flags should be used instead.
 
+``machete.squashMergeDetection``:
+    .. include:: git-config-keys/squashMergeDetection.rst
+
 ``machete.status.extraSpaceBeforeBranchName``:
     .. include:: git-config-keys/status_extraSpaceBeforeBranchName.rst
 
@@ -49,7 +52,6 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
     If you want the worktree to have its own branch layout file (located under ``.git/worktrees/.../machete``),
     set ``git config machete.worktree.useTopLevelMacheteFile false``.
 
-
 **Environment variables:**
 
 ``GIT_MACHETE_EDITOR``
@@ -62,3 +64,6 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
 
 ``GITHUB_TOKEN``
     Used to store GitHub API token. Used by commands such as ``anno --sync-github-prs`` and ``github``.
+
+``GITLAB_TOKEN``
+    Used to store GitLab API token. Used by commands such as ``anno --sync-gitlab-prs`` and ``gitlab``.
diff --git a/docs/source/cli/edit.rst b/docs/source/cli/edit.rst
index a2623a1..d0e21b9 100644
--- a/docs/source/cli/edit.rst
+++ b/docs/source/cli/edit.rst
@@ -12,14 +12,14 @@ Opens an editor and lets you edit the branch layout file manually.
 
 The editor is determined by checking up the following locations:
 
-    * ``$GIT_MACHETE_EDITOR``
-    * ``$GIT_EDITOR``
-    * ``$(git config core.editor)``
-    * ``$VISUAL``
-    * ``$EDITOR``
-    * ``editor``
-    * ``nano``
-    * ``vi``
+* ``$GIT_MACHETE_EDITOR``
+* ``$GIT_EDITOR``
+* ``$(git config core.editor)``
+* ``$VISUAL``
+* ``$EDITOR``
+* ``editor``
+* ``nano``
+* ``vi``
 
 and selecting the first one that is defined and points to an executable file accessible on ``PATH``.
 
diff --git a/docs/source/cli/file.rst b/docs/source/cli/file.rst
index 6589c72..537d195 100644
--- a/docs/source/cli/file.rst
+++ b/docs/source/cli/file.rst
@@ -13,12 +13,12 @@ The file is always called ``machete`` and is located in the git directory of the
 
 Three cases are possible:
 
-    * if ``git machete`` is executed from a regular working directory (not a worktree or submodule),
-      the file is located under ``.git/machete``,
-    * if ``git machete`` is executed from a **worktree**,
-      the file path depends on the ``machete.worktree.useTopLevelMacheteFile`` config key value:
+* if ``git machete`` is executed from a regular working directory (not a worktree or submodule),
+  the file is located under ``.git/machete``,
+* if ``git machete`` is executed from a **worktree**,
+  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 ``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 **submodule**, this file is located in the git folder of the submodule itself under ``.git/modules/.../machete``.
+* if ``git machete`` is executed from a **submodule**, this file is located in the git folder of the submodule itself under ``.git/modules/.../machete``.
diff --git a/docs/source/cli/fork-point.rst b/docs/source/cli/fork-point.rst
index 1407359..19b0fa4 100644
--- a/docs/source/cli/fork-point.rst
+++ b/docs/source/cli/fork-point.rst
@@ -29,9 +29,9 @@ Fork point of the given ``<branch>`` is the commit at which the history of the `
 Fork point is assumed by many ``git machete`` commands as the place where the unique history of the ``<branch>`` starts.
 The range of commits between the fork point and the tip of the given branch is, for instance:
 
-    * listed for each branch by ``git machete status --list-commits``
-    * passed to ``git rebase`` by ``git machete`` ``reapply``/``slide-out``/``traverse``/``update``
-    * provided to ``git diff``/``log`` by ``git machete`` ``diff``/``log``.
+* listed for each branch by ``git machete status --list-commits``
+* passed to ``git rebase`` by ``git machete`` ``reapply``/``slide-out``/``traverse``/``update``
+* provided to ``git diff``/``log`` by ``git machete`` ``diff``/``log``.
 
 ``git machete`` assumes fork point of ``<branch>`` is the most recent commit in the log of ``<branch>`` that has NOT been introduced on that very branch,
 but instead occurs on a reflog (see help for ``git reflog``) of some other branch.
diff --git a/docs/source/cli/go.rst b/docs/source/cli/go.rst
index 8d61016..d55c7b1 100644
--- a/docs/source/cli/go.rst
+++ b/docs/source/cli/go.rst
@@ -12,16 +12,16 @@ where <direction> is one of: ``d[own]``, ``f[irst]``, ``l[ast]``, ``n[ext]``, ``
 
 Checks out the branch specified by the given direction relative to the current branch:
 
-    * ``down``:    the direct children/downstream branch of the current branch.
-    * ``first``:   the first downstream of the root branch of the current branch (like ``root`` followed by ``next``),
-      or the root branch itself if the root has no downstream branches.
-    * ``last``:    the last branch in the branch layout file that has the same root as the current branch;
-      can be the root branch itself if the root has no downstream branches.
-    * ``next``:    the direct successor of the current branch in the branch layout file.
-    * ``prev``:    the direct predecessor of the current branch in the branch layout file.
-    * ``root``:    the root of the tree where the current branch is located.
-      Note: this will typically be something like ``develop`` or ``master``,
-      since all branches are usually meant to be ultimately merged to one of those.
-    * ``up``:      the direct parent/upstream branch of the current branch.
+* ``down``:    the direct children/downstream branch of the current branch.
+* ``first``:   the first downstream of the root branch of the current branch (like ``root`` followed by ``next``),
+  or the root branch itself if the root has no downstream branches.
+* ``last``:    the last branch in the branch layout file that has the same root as the current branch;
+  can be the root branch itself if the root has no downstream branches.
+* ``next``:    the direct successor of the current branch in the branch layout file.
+* ``prev``:    the direct predecessor of the current branch in the branch layout file.
+* ``root``:    the root of the tree where the current branch is located.
+  Note: this will typically be something like ``develop`` or ``master``,
+  since all branches are usually meant to be ultimately merged to one of those.
+* ``up``:      the direct parent/upstream branch of the current branch.
 
 Roughly equivalent to ``git checkout $(git machete show <direction>)``.
diff --git a/docs/source/cli/hooks.rst b/docs/source/cli/hooks.rst
index 018dc15..20ea9b1 100644
--- a/docs/source/cli/hooks.rst
+++ b/docs/source/cli/hooks.rst
@@ -15,19 +15,19 @@ Note: ``hooks`` is not a command as such, just a help topic (there is no ``git m
 
     At least two parameters (branch names) are passed to the hook:
 
-        * <new-upstream> is the upstream of the branch that has been slid out, or in case of multiple branches being slid out
-          --- the upstream of the highest slid out branch;
-        * <lowest-slid-out-branch> is the branch that has been slid out, or in case of multiple branches being slid out --- the lowest slid out branch;
-        * <new-downstreams> are all the following (possibly zero) parameters, which correspond to all original downstreams
-          of <lowest-slid-out-branch>, now reattached as the downstreams of <new-upstream>.
+    * <new-upstream> is the upstream of the branch that has been slid out, or in case of multiple branches being slid out
+      --- the upstream of the highest slid out branch;
+    * <lowest-slid-out-branch> is the branch that has been slid out, or in case of multiple branches being slid out --- the lowest slid out branch;
+    * <new-downstreams> are all the following (possibly zero) parameters, which correspond to all original downstreams
+      of <lowest-slid-out-branch>, now reattached as the downstreams of <new-upstream>.
 
     Note that this may be zero, one, or multiple branches.
 
     Note: the hook, if present, is executed:
 
-        * zero or once during a ``advance`` execution (depending on whether the slide-out has been confirmed or not),
-        * exactly once during a ``slide-out`` execution (even if multiple branches are slid out),
-        * zero or more times during ``traverse`` (every time a slide-out operation is confirmed).
+    * zero or once during a ``advance`` execution (depending on whether the slide-out has been confirmed or not),
+    * exactly once during a ``slide-out`` execution (even if multiple branches are slid out),
+    * zero or more times during ``traverse`` (every time a slide-out operation is confirmed).
 
     If the hook returns a non-zero exit code, then an error is raised and the execution of the command is aborted ---
     ``slide-out`` won't attempt rebase of the new downstream branches and ``traverse`` won't continue the traversal.
diff --git a/docs/source/cli/is-managed.rst b/docs/source/cli/is-managed.rst
index a682a47..355b202 100644
--- a/docs/source/cli/is-managed.rst
+++ b/docs/source/cli/is-managed.rst
@@ -12,6 +12,6 @@ Returns with zero exit code if the given branch (or current branch, if none spec
 
 Returns with a non-zero exit code in case:
 
-    * the <branch> is provided but isn't managed (or doesn't exist), or
-    * the <branch> isn't provided and the current branch isn't managed, or
-    * the <branch> isn't provided and there's no current branch (detached HEAD).
+* the <branch> is provided but isn't managed (or doesn't exist), or
+* the <branch> isn't provided and the current branch isn't managed, or
+* the <branch> isn't provided and there's no current branch (detached HEAD).
diff --git a/docs/source/cli/list.rst b/docs/source/cli/list.rst
index 3c2753c..030d874 100644
--- a/docs/source/cli/list.rst
+++ b/docs/source/cli/list.rst
@@ -12,14 +12,14 @@ where <category> is one of: ``addable``, ``childless``, ``managed``, ``slidable`
 
 Lists all branches that fall into one of the specified categories:
 
-    * ``addable``: all branches (local or remote) than can be added to the branch layout file,
-    * ``childless``: all managed branches that do not possess child branches,
-    * ``managed``: all branches that appear in the branch layout file,
-    * ``slidable``: all managed branches that have an upstream and can be slid out with :ref:`slide-out` command
-    * ``slidable-after <branch>``: the downstream branch of the <branch>, if it exists and is the only downstream of <branch>
-      (and thus can be slid out immediately following <branch>),
-    * ``unmanaged``: all local branches that don't appear in the branch layout file,
-    * ``with-overridden-fork-point``: all local branches that have a :ref:`fork point<fork-point>` override set up
-      (even if this override does not affect the location of their fork point anymore).
+* ``addable``: all branches (local or remote) than can be added to the branch layout file,
+* ``childless``: all managed branches that do not possess child branches,
+* ``managed``: all branches that appear in the branch layout file,
+* ``slidable``: all managed branches that have an upstream and can be slid out with :ref:`slide-out` command
+* ``slidable-after <branch>``: the downstream branch of the <branch>, if it exists and is the only downstream of <branch>
+  (and thus can be slid out immediately following <branch>),
+* ``unmanaged``: all local branches that don't appear in the branch layout file,
+* ``with-overridden-fork-point``: all local branches that have a :ref:`fork point<fork-point>` override set up
+  (even if this override does not affect the location of their fork point anymore).
 
 This command is generally not meant for a day-to-day use, it's mostly needed for the sake of branch name completion in shell.
diff --git a/docs/source/cli/show.rst b/docs/source/cli/show.rst
index 766bf68..da49b59 100644
--- a/docs/source/cli/show.rst
+++ b/docs/source/cli/show.rst
@@ -13,15 +13,15 @@ displayed relative to given <branch>, or the current checked out branch if <bran
 
 Outputs name of the branch (or possibly multiple branches, in case of ``down``) that is:
 
-    * ``current``: the current branch; exits with a non-zero status if none (detached HEAD)
-    * ``down``:    the direct children/downstream branch of the given branch.
-    * ``first``:   the first downstream of the root branch of the given branch (like ``root`` followed by ``next``),
-      or the root branch itself if the root has no downstream branches.
-    * ``last``:    the last branch in the branch layout file that has the same root as the given branch; can be the root branch itself
-      if the root has no downstream branches.
-    * ``next``:    the direct successor of the given branch in the branch layout file.
-    * ``prev``:    the direct predecessor of the given branch in the branch layout file.
-    * ``root``:    the root of the tree where the given branch is located.
-      Note: this will typically be something like ``develop`` or ``master``,
-      since all branches are usually meant to be ultimately merged to one of those.
-    * ``up``:      the direct parent/upstream branch of the given branch.
+* ``current``: the current branch; exits with a non-zero status if none (detached HEAD)
+* ``down``:    the direct children/downstream branch of the given branch.
+* ``first``:   the first downstream of the root branch of the given branch (like ``root`` followed by ``next``),
+  or the root branch itself if the root has no downstream branches.
+* ``last``:    the last branch in the branch layout file that has the same root as the given branch; can be the root branch itself
+  if the root has no downstream branches.
+* ``next``:    the direct successor of the given branch in the branch layout file.
+* ``prev``:    the direct predecessor of the given branch in the branch layout file.
+* ``root``:    the root of the tree where the given branch is located.
+  Note: this will typically be something like ``develop`` or ``master``,
+  since all branches are usually meant to be ultimately merged to one of those.
+* ``up``:      the direct parent/upstream branch of the given branch.
diff --git a/docs/source/cli/slide-out.rst b/docs/source/cli/slide-out.rst
index 3a57016..486626a 100644
--- a/docs/source/cli/slide-out.rst
+++ b/docs/source/cli/slide-out.rst
@@ -7,7 +7,9 @@ slide-out
 .. code-block:: shell
 
     git machete slide-out --removed-from-remote [--delete]
-    git machete slide-out [-d|--down-fork-point=<down-fork-point-commit>] [--delete] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase] [<branch> [<branch> [<branch> ...]]]
+    git machete slide-out [-d|--down-fork-point=<down-fork-point-commit>] [--delete]
+                          [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase]
+                          [<branch> [<branch> [<branch> ...]]]
 
 Removes the given branch (or multiple branches) from the branch layout.
 If no branch has been specified, current branch is slid out.
@@ -75,10 +77,10 @@ See help for :ref:`traverse` for more details on the qualifiers.
 --removed-from-remote                             Slide out managed branches whose remote tracking branches have been deleted and that have no downstreams.
                                                   In other words, this deletes all branches except:
 
-                                                      * those that are unmanaged,
-                                                      * those that have no remote tracking branch set (unpushed),
-                                                      * those whose remote tracking branches still exist (not deleted remotely),
-                                                      * those that have at least one downstream (child) branch.
+                                                  * those that are unmanaged,
+                                                  * those that have no remote tracking branch set (unpushed),
+                                                  * those whose remote tracking branches still exist (not deleted remotely),
+                                                  * those that have at least one downstream (child) branch.
 
 **Environment variables:**
 
diff --git a/docs/source/cli/status.rst b/docs/source/cli/status.rst
index d7c9921..f615fd2 100644
--- a/docs/source/cli/status.rst
+++ b/docs/source/cli/status.rst
@@ -18,35 +18,37 @@ status
 
 .. code-block:: shell
 
-    git machete s[tatus] [--color=WHEN] [-l|--list-commits] [-L|--list-commits-with-hashes] [--no-detect-squash-merges]
+    git machete s[tatus] [--color=WHEN]
+                         [-l|--list-commits] [-L|--list-commits-with-hashes]
+                         [--squash-merge-detection=MODE]
 
 Displays a tree-shaped status of the branches listed in the branch layout file.
 
 Apart from simply ASCII-formatting the branch layout file, this also:
 
-    * colors the edges between upstream (parent) and downstream (children) branches:
+* colors the edges between upstream (parent) and downstream (children) branches:
 
-      - :red:`red edge` means *not in sync*. The downstream branch is NOT a direct descendant of the upstream branch.
+  - :red:`red edge` means *not in sync*. The downstream branch is NOT a direct descendant of the upstream branch.
 
-      - :yellow:`yellow edge` means *in sync but fork point off*. The downstream branch is a direct descendant of the upstream branch,
-        but the :ref:`fork point<fork-point>` of the downstream branch is NOT equal to the upstream branch.
+  - :yellow:`yellow edge` means *in sync but fork point off*. The downstream branch is a direct descendant of the upstream branch,
+    but the :ref:`fork point<fork-point>` of the downstream branch is NOT equal to the upstream branch.
 
-      - :green:`green edge` means *in sync*. The downstream branch is a direct descendant of the upstream branch
-        and the fork point of the downstream branch is equal to the upstream branch.
+  - :green:`green edge` means *in sync*. The downstream branch is a direct descendant of the upstream branch
+    and the fork point of the downstream branch is equal to the upstream branch.
 
-      - :grey:`grey/dimmed edge` means *merged*. 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).
+  - :grey:`grey/dimmed edge` means *merged*. 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;
+* 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 :ref:`format` and :ref:`anno`) next to each branch, if present. Annotations might contain underlined branch
-      qualifiers (``push=no``, ``rebase=no``, ``slide-out=no``) that control rebase and push behavior of ``traverse`` (see help for :ref:`traverse`);
+* displays the custom annotations (see help for :ref:`format` and :ref:`anno`) next to each branch, if present. Annotations might contain underlined branch
+  qualifiers (``push=no``, ``rebase=no``, ``slide-out=no``) that control rebase and push behavior of ``traverse`` (see help for :ref:`traverse`);
 
-    * displays the output of ``machete-status-branch hook`` (see help for :ref:`hooks`), if present;
+* displays the output of ``machete-status-branch hook`` (see help for :ref:`hooks`), if present;
 
-    * optionally lists commits introduced on each branch if ``-l/--list-commits`` or ``-L/--list-commits-with-hashes`` is supplied.
+* optionally lists commits introduced on each branch if ``-l/--list-commits`` or ``-L/--list-commits-with-hashes`` is supplied.
 
 Name of the currently checked-out branch is underlined (or shown in blue on terminals that don't support underline).
 
@@ -83,11 +85,19 @@ When colors are disabled, relation between branches is represented in the follow
 
 -L, --list-commits-with-hashes    Additionally list the short hashes and messages of commits introduced on each branch.
 
---no-detect-squash-merges         Only consider *strict* (fast-forward or 2-parent) merges, rather than rebase/squash merges,
+--no-detect-squash-merges         **Deprecated**, use ``--squash-merge-detection=none`` instead.
+                                  Only consider *strict* (fast-forward or 2-parent) merges, rather than rebase/squash merges,
                                   when detecting if a branch is merged into its upstream (parent).
 
+--squash-merge-detection=MODE     Specify the mode for detection of rebase/squash merges (grey edges).
+                                  ``MODE`` can be ``none`` (fastest, no squash merges are detected), ``simple`` (default) or ``exact`` (slowest).
+                                  See the below paragraph on ``machete.squashMergeDetection`` git config key for more details.
+
 **Git config keys:**
 
+``machete.squashMergeDetection``:
+    .. include:: git-config-keys/squashMergeDetection.rst
+
 ``machete.status.extraSpaceBeforeBranchName``
   .. include:: git-config-keys/status_extraSpaceBeforeBranchName.rst
       :end-line: 3
diff --git a/docs/source/cli/traverse.rst b/docs/source/cli/traverse.rst
index 92c538b..331f6bd 100644
--- a/docs/source/cli/traverse.rst
+++ b/docs/source/cli/traverse.rst
@@ -19,9 +19,9 @@ traverse
 .. code-block:: shell
 
     git machete t[raverse] [-F|--fetch] [-l|--list-commits] [-M|--merge]
-                           [-n|--no-edit-merge|--no-interactive-rebase] [--no-detect-squash-merges]
-                           [--[no-]push] [--[no-]push-untracked]
-                           [--return-to=WHERE] [--start-from=WHERE] [-w|--whole] [-W] [-y|--yes]
+                           [-n|--no-edit-merge|--no-interactive-rebase] [--[no-]push] [--[no-]push-untracked]
+                           [--return-to=WHERE] [--start-from=WHERE] [--squash-merge-detection=MODE]
+                           [-w|--whole] [-W] [-y|--yes]
 
 Traverses the branches in the order as they occur in branch layout file.
 By default, ``traverse`` starts from the current branch.
@@ -29,32 +29,32 @@ This behavior can, however, be customized using options: ``--start-from=``, ``--
 
 For each branch, the command:
 
-    * detects if the branch is merged (:grey:`grey` edge) to its parent (aka upstream):
+* detects if the branch is merged (:grey:`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);
+  - 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` or :yellow:`yellow` edge to its parent/upstream (see help for :ref:`status`):
+* otherwise, if the branch has a :red:`red` or :yellow:`yellow` edge to its parent/upstream (see help for :ref:`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``;
+  - asks the user whether to **rebase** (default) or merge (if ``--merge`` passed) the branch onto into its upstream branch
+    --- equivalent to ``git machete update``;
 
-    * 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:
+* 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);
+  - 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:
+* otherwise, if the branch diverged from the remote counterpart & has older head commit than the counterpart:
 
-      - asks the user whether to **reset** (``git reset --keep``) the branch to its remote counterpart
+  - asks the user whether to **reset** (``git reset --keep``) the branch to its remote counterpart
 
-    * otherwise, if the branch is behind its remote counterpart:
+* otherwise, if the branch is behind its remote counterpart:
 
-      - asks the user whether to **pull** the branch;
+  - asks the user whether to **pull** the branch;
 
-    * and finally, if any of the above operations has been successfully completed:
+* and finally, if any of the above operations has been successfully completed:
 
-      - prints the updated ``status``.
+  - prints the updated ``status``.
 
 By default ``traverse`` asks if the branch should be pushed. This behavior 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
@@ -63,7 +63,7 @@ It can also be customized using options: ``--[no-]push`` or ``--[no-]push-untrac
 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.
 Unlike with ``git rebase`` or ``git cherry-pick``, there is no special ``--continue`` flag, as ``traverse`` is stateless.
-``traverse`` doesn't keep a state of its own like ``git rebase`` does in ``.git/rebase-apply/``.
+``traverse`` does **not** keep a state of its own like ``git rebase`` does in ``.git/rebase-apply/``.
 
 The rebase, push and slide-out behaviors of ``traverse`` can also be customized for each branch separately using *branch qualifiers*.
 There are ``push=no``, ``rebase=no`` and ``slide-out=no`` qualifiers that can be used to opt out of default behavior (rebasing, pushing and sliding the branch out).
@@ -86,50 +86,55 @@ when the current user is NOT the author of the PR/MR associated with that branch
 
 **Options:**
 
--F, --fetch                  Fetch the remotes of all managed branches at the beginning of traversal (no ``git pull`` involved, only ``git fetch``).
+-F, --fetch                    Fetch the remotes of all managed branches at the beginning of traversal (no ``git pull`` involved, only ``git fetch``).
 
--l, --list-commits           When printing the status, additionally list the messages of commits introduced on each branch.
+-l, --list-commits             When printing the status, additionally list the messages of commits introduced on each branch.
 
--M, --merge                  Update by merge rather than by rebase.
+-M, --merge                    Update by merge rather than by rebase.
 
--n                           If updating by rebase, equivalent to ``--no-interactive-rebase``. If updating by merge, equivalent to ``--no-edit-merge``.
+-n                             If updating by rebase, equivalent to ``--no-interactive-rebase``. If updating by merge, equivalent to ``--no-edit-merge``.
 
---no-detect-squash-merges    Only consider *strict* (fast-forward or 2-parent) merges, rather than rebase/squash merges,
-                             when detecting if a branch is merged into its upstream (parent).
+--no-detect-squash-merges      **Deprecated**, use ``--squash-merge-detection=none`` instead.
+                               Only consider *strict* (fast-forward or 2-parent) merges, rather than rebase/squash merges,
+                               when detecting if a branch is merged into its upstream (parent).
 
---no-edit-merge              If updating by merge, skip opening the editor for merge commit message while doing ``git merge``
-                             (that is, pass ``--no-edit`` flag to the underlying ``git merge``). Not allowed if updating by rebase.
+--no-edit-merge                If updating by merge, skip opening the editor for merge commit message while doing ``git merge``
+                               (that is, pass ``--no-edit`` flag to the underlying ``git merge``). Not allowed if updating by rebase.
 
---no-interactive-rebase      If updating by rebase, run ``git rebase`` in non-interactive mode (without ``-i/--interactive`` flag).
-                             Not allowed if updating by merge.
+--no-interactive-rebase        If updating by rebase, run ``git rebase`` in non-interactive mode (without ``-i/--interactive`` flag).
+                               Not allowed if updating by merge.
 
---no-push                    Do not push any (neither tracked nor untracked) branches to remote, re-enable via ``--push``.
+--no-push                      Do not push any (neither tracked nor untracked) branches to remote, re-enable via ``--push``.
 
---no-push-untracked          Do not push untracked branches to remote, re-enable via ``--push-untracked``.
+--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. Default behavior 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                         Push all (both tracked and untracked) branches to remote --- default behavior. Default behavior 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.
+--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``
-                             (nearest remaining branch in case the ``here`` branch has been slid out by the traversal) or
-                             ``stay`` (the default --- just stay wherever the traversal stops). Note: when user quits by ``q``/``yq``
-                             or when traversal is stopped because one of git actions fails, the behavior is always ``stay``.
+--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``
+                               (nearest remaining branch in case the ``here`` branch has been slid out by the traversal) or
+                               ``stay`` (the default --- just stay wherever the traversal stops). Note: when user quits by ``q``/``yq``
+                               or when traversal is stopped because one of git actions fails, the behavior is always ``stay``.
 
---start-from=WHERE           Specifies the branch to start the traversal from; WHERE can be ``here``
-                             (the default --- current branch, must be managed by git machete), ``root`` (root branch of the current branch,
-                             as in ``git machete show root``) or ``first-root`` (first listed managed branch).
+--squash-merge-detection=MODE  Specifies the mode for detection of rebase/squash merges (grey edges).
+                               ``MODE`` can be ``none`` (fastest, no squash merges are detected), ``simple`` (default) or ``exact`` (slowest).
+                               See the below paragraph on ``machete.squashMergeDetection`` git config key for more details.
 
--w, --whole                  Equivalent to ``-n --start-from=first-root --return-to=nearest-remaining``;
-                             useful for quickly traversing & syncing all branches (rather than doing more fine-grained operations on the
-                             local section of the branch tree).
+--start-from=WHERE             Specifies the branch to start the traversal from; WHERE can be ``here``
+                               (the default --- current branch, must be managed by git machete), ``root`` (root branch of the current branch,
+                               as in ``git machete show root``) or ``first-root`` (first listed managed branch).
 
--W                           Equivalent to ``--fetch --whole``; useful for even more automated traversal of all branches.
+-w, --whole                    Equivalent to ``-n --start-from=first-root --return-to=nearest-remaining``;
+                               useful for quickly traversing & syncing all branches (rather than doing more fine-grained operations on the
+                               local section of the branch tree).
 
--y, --yes                    Don't ask for any interactive input, including confirmation of rebase/push/pull. Implies ``-n``.
+-W                             Equivalent to ``--fetch --whole``; useful for even more automated traversal of all branches.
+
+-y, --yes                      Don't ask for any interactive input, including confirmation of rebase/push/pull. Implies ``-n``.
 
 **Environment variables:**
 
@@ -139,5 +144,8 @@ when the current user is NOT the author of the PR/MR associated with that branch
 
 **Git config keys:**
 
+``machete.squashMergeDetection``:
+    .. include:: git-config-keys/squashMergeDetection.rst
+
 ``machete.traverse.push``
     .. include:: git-config-keys/traverse_push.rst
diff --git a/docs/source/cli/update.rst b/docs/source/cli/update.rst
index f30bca4..8749659 100644
--- a/docs/source/cli/update.rst
+++ b/docs/source/cli/update.rst
@@ -6,7 +6,7 @@ update
 
 .. code-block:: shell
 
-    git machete update [-f|--fork-point=] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase]
+    git machete update [-f|--fork-point=<fork-point-commit>] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase]
 
 Synchronizes the current branch with its upstream (parent) branch either by rebase (default) or by merge (if ``--merge`` option passed).
 
diff --git a/docs/source/git-config-keys/squashMergeDetection.rst b/docs/source/git-config-keys/squashMergeDetection.rst
new file mode 100644
index 0000000..7f06e2e
--- /dev/null
+++ b/docs/source/git-config-keys/squashMergeDetection.rst
@@ -0,0 +1,13 @@
+Controls the algorithm used to detect squash merges. Possible values are:
+
+* ``none``: Fastest mode, with no squash merge/rebase detection. Only *strict* (fast-forward or 2-parent) merges are detected.
+* ``simple`` (default): Compares the tree (files & directories in the commit) of the downstream branch with the trees of the upstream branch.
+  This detects squash merges/rebases as long as there exists a squash/rebase commit in the upstream that has the identical tree to what's in the downstream branch.
+* ``exact``: Compares the patch (diff introduced by the commits) of the downstream branch with the patches of the upstream branch.
+  This detects squash merges in more cases than ``simple`` mode.
+  However, it might have a significant performance impact on large repositories as it requires computing patches for commits in the upstream branch.
+
+This has an impact on:
+
+* whether a grey edge is displayed in ``status``,
+* whether ``traverse`` suggests to slide out the branch.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index a6ebd07..fd63ab6 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -14,8 +14,8 @@ Using this tool, you can maintain **small, focused, easy-to-review pull requests
 
 A look at a ``git machete status`` gives an instant answer to the questions:
 
-    * What branches are in this repository?
-    * What is going to be merged (rebased/pushed/pulled) and to what?
+* What branches are in this repository?
+* What is going to be merged (rebased/pushed/pulled) and to what?
 
 ``git machete traverse`` semi-automatically traverses the branches, helping you effortlessly rebase, merge, push and pull.
 
@@ -28,6 +28,14 @@ git machete commands and help topics:
 
 .. include:: learning_materials.rst
 
+General options
+---------------
+
+--debug           Log detailed diagnostic info, including outputs of the executed git commands.
+-h, --help        Print help and exit.
+-v, --verbose     Log the executed git commands.
+--version         Print version and exit.
+
 Commands & help topics
 ----------------------
 
diff --git a/docs/source/learning_materials.rst b/docs/source/learning_materials.rst
index 6753365..4c9e266 100644
--- a/docs/source/learning_materials.rst
+++ b/docs/source/learning_materials.rst
@@ -13,7 +13,7 @@
 
   Check the following blog posts for more information on how to use git machete or how to manage CI in your own project using Docker:
 
-      * `Make your way through the git (rebase) jungle with Git Machete <https://medium.com/virtuslab/make-your-way-through-the-git-rebase-jungle-with-git-machete-e2ed4dbacd02>`_
-      * `git machete strikes again! Traverse the git (rebase) jungle even faster <https://medium.com/virtuslab/git-machete-strikes-again-traverse-the-git-rebase-jungle-even-faster-with-v2-0-f43ebaf8abb0>`_
-      * `Nifty Docker tricks for your CI (vol. 1) <https://medium.com/virtuslab/nifty-docker-tricks-for-your-ci-vol-1-c4a36d2192ea>`_
-      * `Nifty Docker tricks for your CI (vol. 2) <https://medium.com/virtuslab/nifty-docker-tricks-for-your-ci-vol-2-c5191a67f1a4>`_
+  * `Make your way through the git (rebase) jungle with Git Machete <https://medium.com/virtuslab/make-your-way-through-the-git-rebase-jungle-with-git-machete-e2ed4dbacd02>`_
+  * `git machete strikes again! Traverse the git (rebase) jungle even faster <https://medium.com/virtuslab/git-machete-strikes-again-traverse-the-git-rebase-jungle-even-faster-with-v2-0-f43ebaf8abb0>`_
+  * `Nifty Docker tricks for your CI (vol. 1) <https://medium.com/virtuslab/nifty-docker-tricks-for-your-ci-vol-1-c4a36d2192ea>`_
+  * `Nifty Docker tricks for your CI (vol. 2) <https://medium.com/virtuslab/nifty-docker-tricks-for-your-ci-vol-2-c5191a67f1a4>`_
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 6889cb4..c1292df 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.25.3'
+__version__ = '3.26.0'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 7de7057..142e056 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
 
 import git_machete.options
 from git_machete import __version__, git_config_keys, utils
+from git_machete.constants import SquashMergeDetection
 from git_machete.github import GitHubClient
 from git_machete.gitlab import GitLabClient
 
@@ -60,7 +61,7 @@ def get_help_description(display_help_topics: bool, command: Optional[str] = Non
         usage_str += fmt(textwrap.dedent(long_docs[command_by_alias[command]]))
     else:
         usage_str += get_short_general_usage() + '\n' + fmt(
-            "\n<u>TL;DR tip</u>\n\n"
+            "\n<u>Quick start 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:
@@ -70,7 +71,7 @@ def get_help_description(display_help_topics: bool, command: Optional[str] = Non
             usage_str += underline(hdr) + '\n\n'
             for cm in cmds:
                 alias = f", {alias_by_command[cm]}" if cm in alias_by_command else ""
-                usage_str += f'    {bold(cm + alias) : <{18 if utils.ascii_only else 27}}{short_docs[cm]}'
+                usage_str += f'    {bold(cm + alias): <{18 if utils.ascii_only else 27}}{short_docs[cm]}'
                 usage_str += '\n'
             usage_str += '\n'
         usage_str += fmt(textwrap.dedent("""
@@ -364,6 +365,7 @@ def create_cli_parser() -> argparse.ArgumentParser:
     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')
+    status_parser.add_argument('--squash-merge-detection')
 
     traverse_parser = subparsers.add_parser(
         'traverse',
@@ -379,6 +381,7 @@ def create_cli_parser() -> argparse.ArgumentParser:
     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('--squash-merge-detection')
     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')
@@ -455,7 +458,11 @@ def update_cli_options_using_parsed_args(
         elif opt == "n":
             cli_opts.opt_n = True
         elif opt == "no_detect_squash_merges":
-            cli_opts.opt_no_detect_squash_merges = True
+            warn("`--no-detect-squash-merges` is deprecated, use `--squash-merge-detection=none` instead", end="\n\n")
+            cli_opts.opt_squash_merge_detection_string = "none"
+        elif opt == "squash_merge_detection" and arg is not None:  # if no arg is passed, argparse will fail anyway
+            cli_opts.opt_squash_merge_detection_string = arg
+            cli_opts.opt_squash_merge_detection_origin = "`--squash-merge-detection` flag"
         elif opt == "no_edit_merge":
             cli_opts.opt_no_edit_merge = True
         elif opt == "no_interactive_rebase":
@@ -529,6 +536,13 @@ def update_cli_options_using_config_keys(
         else:
             cli_opts.opt_push_tracked, cli_opts.opt_push_untracked = False, False
 
+    squash_merge_detection = git.get_config_attr_or_none(key=git_config_keys.SQUASH_MERGE_DETECTION)
+    if squash_merge_detection is not None:
+        # Let's defer the validation until the value is actually used in `status` or `traverse`.
+        # Otherwise, if an invalid value ends up in git config, `git machete help` will instantly fail.
+        cli_opts.opt_squash_merge_detection_string = squash_merge_detection
+        cli_opts.opt_squash_merge_detection_origin = f"`{git_config_keys.SQUASH_MERGE_DETECTION}` git config key"
+
 
 def set_utils_global_variables(parsed_args: argparse.Namespace) -> None:
     args = vars(parsed_args)
@@ -861,34 +875,38 @@ def launch(orig_args: List[str]) -> None:
                 )
             machete_client.squash(current_branch=current_branch, opt_fork_point=squash_fork_point)
         elif cmd in {"status", alias_by_command["status"]}:
+            opt_squash_merge_detection = SquashMergeDetection.from_string(
+                cli_opts.opt_squash_merge_detection_string, cli_opts.opt_squash_merge_detection_origin)
+
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             machete_client.expect_at_least_one_managed_branch()
             machete_client.status(
                 warn_when_branch_in_sync_but_fork_point_off=True,
                 opt_list_commits=cli_opts.opt_list_commits,
                 opt_list_commits_with_hashes=cli_opts.opt_list_commits_with_hashes,
-                opt_no_detect_squash_merges=cli_opts.opt_no_detect_squash_merges)
+                opt_squash_merge_detection=opt_squash_merge_detection)
         elif cmd in {"traverse", alias_by_command["traverse"]}:
+            if cli_opts.opt_return_to not in {"here", "nearest-remaining", "stay"}:
+                raise MacheteException(f"Invalid value for `--return-to` flag: `{cli_opts.opt_return_to}`. "
+                                       "Valid values are here, nearest-remaining, stay")
             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`.")
-            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`.")
+                raise MacheteException(f"Invalid value for `--start-from` flag: `{cli_opts.opt_start_from}`. "
+                                       "Valid values are here, root, first-root")
+            opt_squash_merge_detection = SquashMergeDetection.from_string(
+                cli_opts.opt_squash_merge_detection_string, cli_opts.opt_squash_merge_detection_origin)
+
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             machete_client.traverse(
                 opt_fetch=cli_opts.opt_fetch,
                 opt_list_commits=cli_opts.opt_list_commits,
                 opt_merge=cli_opts.opt_merge,
-                opt_no_detect_squash_merges=cli_opts.opt_no_detect_squash_merges,
                 opt_no_edit_merge=cli_opts.opt_no_edit_merge,
                 opt_no_interactive_rebase=cli_opts.opt_no_interactive_rebase,
                 opt_push_tracked=cli_opts.opt_push_tracked,
                 opt_push_untracked=cli_opts.opt_push_untracked,
                 opt_return_to=cli_opts.opt_return_to,
+                opt_squash_merge_detection=opt_squash_merge_detection,
                 opt_start_from=cli_opts.opt_start_from,
                 opt_yes=cli_opts.opt_yes)
         elif cmd == "update":
diff --git a/git_machete/client.py b/git_machete/client.py
index 7e8782d..742a326 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -17,7 +17,7 @@ from .code_hosting import (CodeHostingClient, CodeHostingSpec,
                            is_matching_remote_url)
 from .constants import (DISCOVER_DEFAULT_FRESH_BRANCH_COUNT, PICK_FIRST_ROOT,
                         PICK_LAST_ROOT, GitFormatPatterns,
-                        SyncToRemoteStatuses)
+                        SquashMergeDetection, SyncToRemoteStatuses)
 from .exceptions import (InteractionStopped, MacheteException,
                          UnexpectedMacheteException)
 from .git_operations import (HEAD, AnyBranchName, AnyRevision, BranchPair,
@@ -463,7 +463,7 @@ class MacheteClient:
                 if self.is_merged_to(
                         branch=branch,
                         upstream=upstream,
-                        opt_no_detect_squash_merges=False
+                        opt_squash_merge_detection=SquashMergeDetection.NONE
                 ):
                     debug(f"inferred upstream of {branch} is {upstream}, but "
                           f"{branch} is merged to {upstream}; skipping {branch} from discovered tree")
@@ -489,7 +489,7 @@ class MacheteClient:
             warn_when_branch_in_sync_but_fork_point_off=False,
             opt_list_commits=opt_list_commits,
             opt_list_commits_with_hashes=False,
-            opt_no_detect_squash_merges=False)
+            opt_squash_merge_detection=SquashMergeDetection.NONE)
         print("")
         do_backup = os.path.isfile(self.__branch_layout_file_path) and io.open(self.__branch_layout_file_path).read().strip()
         backup_msg = (
@@ -611,7 +611,7 @@ class MacheteClient:
 
         def connected_with_green_edge(bd: LocalBranchShortName) -> bool:
             return bool(
-                not self.__is_merged_to_upstream(bd, opt_no_detect_squash_merges=False) and
+                not self.__is_merged_to_upstream(bd, opt_squash_merge_detection=SquashMergeDetection.NONE) and
                 self.__git.is_ancestor_or_equal(branch.full_name(), bd.full_name()) and
                 (self.__get_overridden_fork_point(bd) or
                  self.__git.get_commit_hash_by_revision(branch) == self.fork_point(bd, use_overrides=False)))
@@ -685,12 +685,12 @@ class MacheteClient:
             opt_fetch: bool,
             opt_list_commits: bool,
             opt_merge: bool,
-            opt_no_detect_squash_merges: bool,
             opt_no_edit_merge: bool,
             opt_no_interactive_rebase: bool,
             opt_push_tracked: bool,
             opt_push_untracked: bool,
             opt_return_to: str,
+            opt_squash_merge_detection: SquashMergeDetection,
             opt_start_from: str,
             opt_yes: bool
     ) -> None:
@@ -730,7 +730,7 @@ class MacheteClient:
             upstream = self.__up_branch.get(branch)
 
             needs_slide_out: bool = self.__is_merged_to_upstream(
-                branch, opt_no_detect_squash_merges=opt_no_detect_squash_merges)
+                branch, opt_squash_merge_detection=opt_squash_merge_detection)
             if needs_slide_out and branch in self.annotations:
                 needs_slide_out = self.annotations[branch].qualifiers.slide_out
             s, remote = self.__git.get_combined_remote_sync_status(branch)
@@ -785,7 +785,7 @@ class MacheteClient:
                     warn_when_branch_in_sync_but_fork_point_off=True,
                     opt_list_commits=opt_list_commits,
                     opt_list_commits_with_hashes=False,
-                    opt_no_detect_squash_merges=opt_no_detect_squash_merges)
+                    opt_squash_merge_detection=opt_squash_merge_detection)
                 self.__print_new_line(True)
             if needs_slide_out:
                 any_action_suggested = True
@@ -944,7 +944,7 @@ class MacheteClient:
             warn_when_branch_in_sync_but_fork_point_off=True,
             opt_list_commits=opt_list_commits,
             opt_list_commits_with_hashes=False,
-            opt_no_detect_squash_merges=opt_no_detect_squash_merges)
+            opt_squash_merge_detection=opt_squash_merge_detection)
         print("")
         if current_branch == self.managed_branches[-1]:
             msg: str = f"Reached branch {bold(current_branch)} which has no successor"
@@ -969,7 +969,7 @@ class MacheteClient:
             warn_when_branch_in_sync_but_fork_point_off: bool,
             opt_list_commits: bool,
             opt_list_commits_with_hashes: bool,
-            opt_no_detect_squash_merges: bool
+            opt_squash_merge_detection: SquashMergeDetection
     ) -> None:
         next_sibling_of_ancestor_by_branch: OrderedDict[LocalBranchShortName, List[Optional[LocalBranchShortName]]] = OrderedDict()
 
@@ -1009,7 +1009,7 @@ class MacheteClient:
             if self.is_merged_to(
                     branch=branch,
                     upstream=parent_branch,
-                    opt_no_detect_squash_merges=opt_no_detect_squash_merges):
+                    opt_squash_merge_detection=opt_squash_merge_detection):
                 sync_to_parent_status[branch] = SyncToParentStatus.MergedToParent
             elif not self.__git.is_ancestor_or_equal(parent_branch.full_name(), branch.full_name()):
                 sync_to_parent_status[branch] = SyncToParentStatus.OutOfSync
@@ -1065,8 +1065,8 @@ class MacheteClient:
                             right_arrow = colored(utils.get_right_arrow(), AnsiEscapeCodes.RED)
                             fork_point_str = colored("fork point ???", AnsiEscapeCodes.RED)
                             fp_suffix: str = f' {right_arrow} {fork_point_str} ' + \
-                                             ("this commit" if opt_list_commits_with_hashes else f"commit {commit.short_hash}") + \
-                                             f' seems to be a part of the unique history of {fp_branches_formatted}'
+                                ("this commit" if opt_list_commits_with_hashes else f"commit {commit.short_hash}") + \
+                                f' seems to be a part of the unique history of {fp_branches_formatted}'
                         else:
                             fp_suffix = ''
                         print_line_prefix(branch, utils.get_vertical_bar())
@@ -1159,7 +1159,7 @@ class MacheteClient:
             else:
                 affected_branches = ", ".join(map(bold, branches_in_sync_but_fork_point_off))
                 first_part = f"yellow edges indicate that fork points for {affected_branches} are probably incorrectly inferred,\n" \
-                             "or that some extra branch should be added between each of these branches and its parent"
+                    "or that some extra branch should be added between each of these branches and its parent"
 
             if not opt_list_commits:
                 second_part = "Run `git machete status --list-commits` or " \
@@ -1527,11 +1527,11 @@ class MacheteClient:
         return []
 
     def __is_merged_to_upstream(
-            self, branch: LocalBranchShortName, *, opt_no_detect_squash_merges: bool) -> bool:
+            self, branch: LocalBranchShortName, *, opt_squash_merge_detection: SquashMergeDetection) -> bool:
         upstream = self.__up_branch.get(branch)
         if not upstream:
             return False
-        return self.is_merged_to(branch, upstream, opt_no_detect_squash_merges=opt_no_detect_squash_merges)
+        return self.is_merged_to(branch, upstream, opt_squash_merge_detection=opt_squash_merge_detection)
 
     def __run_post_slide_out_hook(self, new_upstream: LocalBranchShortName, slid_out_branch: LocalBranchShortName,
                                   new_downstreams: List[LocalBranchShortName]) -> None:
@@ -1866,11 +1866,13 @@ class MacheteClient:
     ) -> None:
         rems = self.__git.get_remotes()
         print("\n".join(f"[{index + 1}] {rem}" for index, rem in enumerate(rems)))
-        msg = f"Select number 1..{len(rems)} to specify the destination remote " \
-              "repository, or 'n' to skip this branch, or " \
-              "'q' to quit the traverse: " if is_called_from_traverse \
-            else f"Select number 1..{len(rems)} to specify the destination remote " \
-                 "repository, or 'q' to quit the operation: "
+        if is_called_from_traverse:
+            msg = f"Select number 1..{len(rems)} to specify the destination remote " \
+                "repository, or 'n' to skip this branch, or " \
+                "'q' to quit the traverse: "
+        else:
+            msg = f"Select number 1..{len(rems)} to specify the destination remote " \
+                "repository, or 'q' to quit the operation: "
 
         ans = input(msg).lower()
         if ans in ('q', 'quit'):
@@ -2012,7 +2014,12 @@ class MacheteClient:
         elif ans in ('q', 'quit'):
             raise InteractionStopped
 
-    def is_merged_to(self, branch: LocalBranchShortName, upstream: AnyBranchName, *, opt_no_detect_squash_merges: bool) -> bool:
+    def is_merged_to(
+            self,
+            branch: LocalBranchShortName,
+            upstream: AnyBranchName,
+            opt_squash_merge_detection: SquashMergeDetection
+    ) -> bool:
         if self.__git.is_ancestor_or_equal(branch.full_name(), upstream.full_name()):
             # If branch is ancestor of or equal to the upstream, we need to distinguish between the
             # case of branch being "recently" created from the upstream and the case of
@@ -2021,13 +2028,19 @@ class MacheteClient:
             # (reflog stripped of trivial events like branch creation, reset etc.)
             # is non-empty.
             return bool(self.filtered_reflog(branch))
-        elif opt_no_detect_squash_merges:
+        elif opt_squash_merge_detection == SquashMergeDetection.NONE:
             return False
-        else:
+        elif opt_squash_merge_detection == SquashMergeDetection.SIMPLE:
             # In the default mode.
             # If a commit with an identical tree state to branch is reachable from upstream,
             # then branch may have been squashed or rebase-merged into upstream.
             return self.__git.is_equivalent_tree_reachable(branch, upstream)
+        elif opt_squash_merge_detection == SquashMergeDetection.EXACT:
+            # Let's try another way, a little more complex but takes into account the possibility
+            # that there were other commits between the common ancestor of the two branches and the squashed merge.
+            return self.__git.is_equivalent_tree_reachable(branch, upstream) or self.__git.is_equivalent_patch_reachable(branch, upstream)
+        else:  # pragma: no cover
+            raise UnexpectedMacheteException(f"Invalid squash merged detection mode: {opt_squash_merge_detection}.")
 
     @staticmethod
     def ask_if(
@@ -2321,7 +2334,7 @@ class MacheteClient:
                     warn_when_branch_in_sync_but_fork_point_off=True,
                     opt_list_commits=False,
                     opt_list_commits_with_hashes=False,
-                    opt_no_detect_squash_merges=False)
+                    opt_squash_merge_detection=SquashMergeDetection.NONE)
                 self.__print_new_line(False)
 
                 if converted_to_draft:
@@ -2855,7 +2868,7 @@ class MacheteClient:
                     warn_when_branch_in_sync_but_fork_point_off=True,
                     opt_list_commits=False,
                     opt_list_commits_with_hashes=False,
-                    opt_no_detect_squash_merges=False)
+                    opt_squash_merge_detection=SquashMergeDetection.NONE)
                 self.__print_new_line(False)
 
         else:
diff --git a/git_machete/constants.py b/git_machete/constants.py
index 5e20ec2..54c6156 100644
--- a/git_machete/constants.py
+++ b/git_machete/constants.py
@@ -1,7 +1,11 @@
 from enum import Enum, IntEnum
+from typing import Optional
+
+from git_machete.exceptions import MacheteException
 
-MAX_COUNT_FOR_INITIAL_LOG = 10
 DISCOVER_DEFAULT_FRESH_BRANCH_COUNT = 10
+MAX_COMMITS_FOR_SQUASH_MERGE_DETECTION = 1000
+MAX_COUNT_FOR_INITIAL_LOG = 10
 
 PICK_FIRST_ROOT: int = 0
 PICK_LAST_ROOT: int = -1
@@ -27,3 +31,18 @@ class GitFormatPatterns(Enum):
     FULL_MESSAGE = "%B"
     # subject NOT included
     MESSAGE_BODY = "%b"
+
+
+class SquashMergeDetection(Enum):
+    NONE = "none"
+    SIMPLE = "simple"
+    EXACT = "exact"
+
+    @staticmethod
+    def from_string(value: str, from_where: Optional[str]) -> 'SquashMergeDetection':
+        try:
+            return SquashMergeDetection[value.upper()]
+        except KeyError:
+            valid_values = ', '.join(e.value for e in SquashMergeDetection)
+            prefix = f"Invalid value for {from_where}" if from_where else "Invalid value"
+            raise MacheteException(f"{prefix}: `{value}`. Valid values are `{valid_values}`")
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index ebc99ca..8a986bc 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -285,6 +285,25 @@ long_docs: Dict[str, str] = {
               There should be <b>no</b> need for the user to interact with this key directly,
               `git machete fork-point` with flags should be used instead.
 
+           `machete.squashMergeDetection`:
+ 
+              Controls the algorithm used to detect squash merges. Possible values are:
+
+                 * `none`: Fastest mode, with no squash merge/rebase detection. Only strict (fast-forward or 2-parent) merges are detected.
+
+                 * `simple` (default): Compares the tree (files & directories in the commit) of the downstream branch with the trees of the upstream branch.
+                   This detects squash merges/rebases as long as there exists a squash/rebase commit in the upstream that has the identical tree to what's in the downstream branch.
+
+                 * `exact`: Compares the patch (diff introduced by the commits) of the downstream branch with the patches of the upstream branch.
+                   This detects squash merges in more cases than `simple` mode.
+                   However, it might have a significant performance impact on large repositories as it requires computing patches for commits in the upstream branch.
+
+              This has an impact on:
+
+                 * whether a grey edge is displayed in `status`,
+
+                 * whether `traverse` suggests to slide out the branch.
+
            `machete.status.extraSpaceBeforeBranchName`:
  
               To make it easier to select branch name from the `status` output on certain terminals
@@ -334,6 +353,9 @@ long_docs: Dict[str, str] = {
            `GITHUB_TOKEN`
               Used to store GitHub API token. Used by commands such as `anno --sync-github-prs` and `github`.
 
+           `GITLAB_TOKEN`
+              Used to store GitLab API token. Used by commands such as `anno --sync-gitlab-prs` and `gitlab`.
+
    """,
     "delete-unmanaged": """
         <b>Usage:</b><b>
@@ -1053,7 +1075,9 @@ long_docs: Dict[str, str] = {
     "slide-out": """
         <b>Usage:</b><b>
            git machete slide-out --removed-from-remote [--delete]
-           git machete slide-out [-d|--down-fork-point=<down-fork-point-commit>] [--delete] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase] [<branch> [<branch> [<branch> ...]]]</b>
+           git machete slide-out [-d|--down-fork-point=<down-fork-point-commit>] [--delete]
+                                 [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase]
+                                 [<branch> [<branch> [<branch> ...]]]</b>
 
         Removes the given branch (or multiple branches) from the branch layout.
         If no branch has been specified, current branch is slid out.
@@ -1155,7 +1179,9 @@ long_docs: Dict[str, str] = {
    """,
     "status": """
         <b>Usage:</b><b>
-           git machete s[tatus] [--color=WHEN] [-l|--list-commits] [-L|--list-commits-with-hashes] [--no-detect-squash-merges]</b>
+           git machete s[tatus] [--color=WHEN]
+                                [-l|--list-commits] [-L|--list-commits-with-hashes]
+                                [--squash-merge-detection=MODE]</b>
 
         Displays a tree-shaped status of the branches listed in the branch layout file.
 
@@ -1229,10 +1255,34 @@ long_docs: Dict[str, str] = {
            <b>-L</b>, <b>--list-commits-with-hashes</b>
               Additionally list the short hashes and messages of commits introduced on each branch.
            <b>--no-detect-squash-merges</b>
+              <b>Deprecated</b>, use `--squash-merge-detection=none` instead.
               Only consider strict (fast-forward or 2-parent) merges, rather than rebase/squash merges,
               when detecting if a branch is merged into its upstream (parent).
+           <b>--squash-merge-detection=MODE</b>
+              Specify the mode for detection of rebase/squash merges (grey edges).
+              `MODE` can be `none` (fastest, no squash merges are detected), `simple` (default) or `exact` (slowest).
+              See the below paragraph on `machete.squashMergeDetection` git config key for more details.
 
         <b>Git config keys:</b>
+           `machete.squashMergeDetection`:
+ 
+              Controls the algorithm used to detect squash merges. Possible values are:
+
+                 * `none`: Fastest mode, with no squash merge/rebase detection. Only strict (fast-forward or 2-parent) merges are detected.
+
+                 * `simple` (default): Compares the tree (files & directories in the commit) of the downstream branch with the trees of the upstream branch.
+                   This detects squash merges/rebases as long as there exists a squash/rebase commit in the upstream that has the identical tree to what's in the downstream branch.
+
+                 * `exact`: Compares the patch (diff introduced by the commits) of the downstream branch with the patches of the upstream branch.
+                   This detects squash merges in more cases than `simple` mode.
+                   However, it might have a significant performance impact on large repositories as it requires computing patches for commits in the upstream branch.
+
+              This has an impact on:
+
+                 * whether a grey edge is displayed in `status`,
+
+                 * whether `traverse` suggests to slide out the branch.
+
            `machete.status.extraSpaceBeforeBranchName`
  
               To make it easier to select branch name from the `status` output on certain terminals
@@ -1259,9 +1309,9 @@ long_docs: Dict[str, str] = {
     "traverse": """
         <b>Usage:</b><b>
            git machete t[raverse] [-F|--fetch] [-l|--list-commits] [-M|--merge]
-                                  [-n|--no-edit-merge|--no-interactive-rebase] [--no-detect-squash-merges]
-                                  [--[no-]push] [--[no-]push-untracked]
-                                  [--return-to=WHERE] [--start-from=WHERE] [-w|--whole] [-W] [-y|--yes]</b>
+                                  [-n|--no-edit-merge|--no-interactive-rebase] [--[no-]push] [--[no-]push-untracked]
+                                  [--return-to=WHERE] [--start-from=WHERE] [--squash-merge-detection=MODE]
+                                  [-w|--whole] [-W] [-y|--yes]</b>
 
         Traverses the branches in the order as they occur in branch layout file.
         By default, `traverse` starts from the current branch.
@@ -1291,7 +1341,7 @@ long_docs: Dict[str, str] = {
         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.
         Unlike with `git rebase` or `git cherry-pick`, there is no special `--continue` flag, as `traverse` is stateless.
-        `traverse` doesn't keep a state of its own like `git rebase` does in `.git/rebase-apply/`.
+        `traverse` does <b>not</b> keep a state of its own like `git rebase` does in `.git/rebase-apply/`.
 
         The rebase, push and slide-out behaviors of `traverse` can also be customized for each branch separately using branch qualifiers.
         There are `push=no`, `rebase=no` and `slide-out=no` qualifiers that can be used to opt out of default behavior (rebasing, pushing and sliding the branch out).
@@ -1320,6 +1370,7 @@ long_docs: Dict[str, str] = {
            <b>-n</b>
               If updating by rebase, equivalent to `--no-interactive-rebase`. If updating by merge, equivalent to `--no-edit-merge`.
            <b>--no-detect-squash-merges</b>
+              <b>Deprecated</b>, use `--squash-merge-detection=none` instead.
               Only consider strict (fast-forward or 2-parent) merges, rather than rebase/squash merges,
               when detecting if a branch is merged into its upstream (parent).
            <b>--no-edit-merge</b>
@@ -1344,6 +1395,10 @@ long_docs: Dict[str, str] = {
               (nearest remaining branch in case the `here` branch has been slid out by the traversal) or
               `stay` (the default — just stay wherever the traversal stops). Note: when user quits by `q`/`yq`
               or when traversal is stopped because one of git actions fails, the behavior is always `stay`.
+           <b>--squash-merge-detection=MODE</b>
+              Specifies the mode for detection of rebase/squash merges (grey edges).
+              `MODE` can be `none` (fastest, no squash merges are detected), `simple` (default) or `exact` (slowest).
+              See the below paragraph on `machete.squashMergeDetection` git config key for more details.
            <b>--start-from=WHERE</b>
               Specifies the branch to start the traversal from; WHERE can be `here`
               (the default — current branch, must be managed by git machete), `root` (root branch of the current branch,
@@ -1363,6 +1418,25 @@ long_docs: Dict[str, str] = {
               Example: `GIT_MACHETE_REBASE_OPTS="--keep-empty --rebase-merges" git machete traverse`.
 
         <b>Git config keys:</b>
+           `machete.squashMergeDetection`:
+ 
+              Controls the algorithm used to detect squash merges. Possible values are:
+
+                 * `none`: Fastest mode, with no squash merge/rebase detection. Only strict (fast-forward or 2-parent) merges are detected.
+
+                 * `simple` (default): Compares the tree (files & directories in the commit) of the downstream branch with the trees of the upstream branch.
+                   This detects squash merges/rebases as long as there exists a squash/rebase commit in the upstream that has the identical tree to what's in the downstream branch.
+
+                 * `exact`: Compares the patch (diff introduced by the commits) of the downstream branch with the patches of the upstream branch.
+                   This detects squash merges in more cases than `simple` mode.
+                   However, it might have a significant performance impact on large repositories as it requires computing patches for commits in the upstream branch.
+
+              This has an impact on:
+
+                 * whether a grey edge is displayed in `status`,
+
+                 * whether `traverse` suggests to slide out the branch.
+
            `machete.traverse.push`
  
               To change the behavior of `git machete traverse` command so that it doesn't push branches by default,
@@ -1373,7 +1447,7 @@ long_docs: Dict[str, str] = {
    """,
     "update": """
         <b>Usage:</b><b>
-           git machete update [-f|--fork-point=] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase]</b>
+           git machete update [-f|--fork-point=<fork-point-commit>] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase]</b>
 
         Synchronizes the current branch with its upstream (parent) branch either by rebase (default) or by merge (if `--merge` option passed).
 
diff --git a/git_machete/git_config_keys.py b/git_machete/git_config_keys.py
index 9aaa29e..63d290c 100644
--- a/git_machete/git_config_keys.py
+++ b/git_machete/git_config_keys.py
@@ -1,6 +1,7 @@
 STATUS_EXTRA_SPACE_BEFORE_BRANCH_NAME = 'machete.status.extraSpaceBeforeBranchName'
 TRAVERSE_PUSH = 'machete.traverse.push'
 WORKTREE_USE_TOP_LEVEL_MACHETE_FILE = 'machete.worktree.useTopLevelMacheteFile'
+SQUASH_MERGE_DETECTION = 'machete.squashMergeDetection'
 
 
 def override_fork_point_to(branch: str) -> str:
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index b13c1bc..ec6f81b 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -8,7 +8,8 @@ from typing import (Any, Dict, Iterator, List, Match, NamedTuple, Optional,
                     Set, Tuple)
 
 from . import utils
-from .constants import (MAX_COUNT_FOR_INITIAL_LOG, GitFormatPatterns,
+from .constants import (MAX_COMMITS_FOR_SQUASH_MERGE_DETECTION,
+                        MAX_COUNT_FOR_INITIAL_LOG, GitFormatPatterns,
                         SyncToRemoteStatuses)
 from .exceptions import UnderlyingGitException, UnexpectedMacheteException
 from .utils import (AnsiEscapeCodes, CommandResult, colored, debug, fmt,
@@ -148,6 +149,15 @@ class FullTreeHash(str):
         return FullTreeHash(value)
 
 
+class FullPatchId(str):
+    @staticmethod
+    def of(value: str) -> Optional["FullPatchId"]:
+        if not value:
+            raise UnexpectedMacheteException(
+                f'FullPatchId.of should not accept {value} as a param.')
+        return FullPatchId(value)
+
+
 class ForkPointOverrideData:
     def __init__(self, to_hash: FullCommitHash):
         self.to_hash: FullCommitHash = to_hash
@@ -188,6 +198,7 @@ class GitContext:
         self.__counterparts_for_fetching_cached: Optional[Dict[LocalBranchShortName, Optional[RemoteBranchShortName]]] = None
         self.__fetch_done_for: Set[str] = set()
         self.__initial_log_hashes_cached: Dict[FullCommitHash, List[FullCommitHash]] = {}
+        self.__is_equivalent_patch_reachable_cached: Dict[Tuple[FullCommitHash, FullCommitHash], bool] = {}
         self.__is_equivalent_tree_reachable_cached: Dict[Tuple[FullCommitHash, FullCommitHash], bool] = {}
         self.__local_branches_cached: Optional[List[LocalBranchShortName]] = None
         self.__merge_base_cached: Dict[Tuple[FullCommitHash, FullCommitHash], Optional[FullCommitHash]] = {}
@@ -223,8 +234,8 @@ class GitContext:
         return exit_code
 
     def _popen_git(self, git_cmd: str, *args: str,
-                   allow_non_zero: bool = False, env: Optional[Dict[str, str]] = None) -> CommandResult:
-        exit_code, stdout, stderr = utils.popen_cmd("git", git_cmd, *args, env=env)
+                   allow_non_zero: bool = False, env: Optional[Dict[str, str]] = None, input: Optional[str] = None) -> CommandResult:
+        exit_code, stdout, stderr = utils.popen_cmd("git", git_cmd, *args, env=env, input=input)
         if not allow_non_zero and exit_code != 0:
             exit_code_msg: str = fmt(f"`{utils.get_cmd_shell_repr('git', git_cmd, *args, env=env)}` returned {exit_code}\n")
             stdout_msg: str = f"\n{utils.bold('stdout')}:\n{utils.dim(stdout)}" if stdout else ""
@@ -790,6 +801,8 @@ class GitContext:
         equivalent_to_commit_hash = self.get_commit_hash_by_revision(equivalent_to)
         reachable_from_commit_hash = self.get_commit_hash_by_revision(reachable_from)
         if not equivalent_to_commit_hash or not reachable_from_commit_hash:
+            # Case not covered by tests, unlikely to be reached by an actual execution.
+            # Mostly here to satisfy mypy.
             return False
 
         if equivalent_to_commit_hash == reachable_from_commit_hash:
@@ -798,11 +811,11 @@ class GitContext:
         if (equivalent_to_commit_hash, reachable_from_commit_hash) in self.__is_equivalent_tree_reachable_cached:
             return self.__is_equivalent_tree_reachable_cached[equivalent_to_commit_hash, reachable_from_commit_hash]
 
-        earlier_tree_hash = self.get_tree_hash_by_commit_hash(equivalent_to_commit_hash)
+        tree_hash_for_equivalent_to = self.get_tree_hash_by_commit_hash(equivalent_to_commit_hash)
 
         # `git log ^equivalent_to_commit_hash reachable_from_commit_hash`
         # shows all commits reachable from reachable_from_commit_hash but NOT from equivalent_to_commit_hash
-        intermediate_tree_hashes = utils.get_non_empty_lines(
+        tree_hashes_for_reachable_from = utils.get_non_empty_lines(
             self._popen_git(
                 "log",
                 "--format=%T",  # full commit's tree hash
@@ -811,11 +824,72 @@ class GitContext:
             ).stdout
         )
 
-        result = earlier_tree_hash in intermediate_tree_hashes
-        debug(f"result = {result}")
+        result = tree_hash_for_equivalent_to in tree_hashes_for_reachable_from
+        debug(f"tree_hash_for_equivalent_to in tree_hashes_for_reachable_from = {result}")
         self.__is_equivalent_tree_reachable_cached[equivalent_to_commit_hash, reachable_from_commit_hash] = result
         return result
 
+    def is_equivalent_patch_reachable(
+            self,
+            equivalent_to: AnyRevision,
+            reachable_from: AnyRevision
+    ) -> bool:
+        equivalent_to_commit_hash = self.get_commit_hash_by_revision(equivalent_to)
+        reachable_from_commit_hash = self.get_commit_hash_by_revision(reachable_from)
+        if not equivalent_to_commit_hash or not reachable_from_commit_hash:
+            # Case not covered by tests, unlikely to be reached by an actual execution.
+            # Mostly here to satisfy mypy.
+            return False
+
+        if equivalent_to_commit_hash == reachable_from_commit_hash:
+            return True
+
+        if (equivalent_to_commit_hash, reachable_from_commit_hash) in self.__is_equivalent_patch_reachable_cached:
+            return self.__is_equivalent_patch_reachable_cached[equivalent_to_commit_hash, reachable_from_commit_hash]
+
+        common_ancestor = self.get_merge_base(reachable_from_commit_hash, equivalent_to_commit_hash)
+        if not common_ancestor:
+            return False
+
+        changes_of_equivalent_to = self._popen_git(
+            "diff",
+            common_ancestor,
+            equivalent_to_commit_hash
+        ).stdout
+        if changes_of_equivalent_to.strip() == '':
+            # Empty changeset means the branches are identical, so the tree is equivalent.
+            self.__is_equivalent_patch_reachable_cached[equivalent_to_commit_hash, reachable_from_commit_hash] = True
+            return True
+
+        patch_id_for_changes_of_equivalent_to: Optional[FullPatchId] = self.__get_patch_id_for_diff(changes_of_equivalent_to)
+        patch_ids_for_commits_of_reachable_from: Set[FullPatchId] = set(self.__get_patch_ids_for_commits_between(
+            common_ancestor, reachable_from_commit_hash, MAX_COMMITS_FOR_SQUASH_MERGE_DETECTION).values())
+        result = patch_id_for_changes_of_equivalent_to in patch_ids_for_commits_of_reachable_from
+        debug(f"patch_id_for_changes_of_equivalent_to in patch_ids_for_commits_of_reachable_from = {result}")
+        self.__is_equivalent_patch_reachable_cached[equivalent_to_commit_hash, reachable_from_commit_hash] = result
+        return result
+
+    def __get_patch_id_for_diff(self, patch_contents: str) -> Optional[FullPatchId]:
+        out = utils.get_non_empty_lines(self._popen_git("patch-id", input=patch_contents).stdout)
+
+        if len(out) == 0:
+            # Line uncovered as we actually always pass a non-empty patch to this method.
+            return None
+        return FullPatchId.of(out[0].split(' ')[0])  # patch-id output is "<patch-id> <commit-hash>", we only care about the patch-id
+
+    def __get_patch_ids_for_commits_between(
+            self, earliest_exclusive: AnyRevision, latest_inclusive: AnyRevision, max_commits: int
+    ) -> Dict[FullCommitHash, FullPatchId]:
+        patches = self._popen_git("log", "--patch", f"^{earliest_exclusive}", latest_inclusive, f"-{max_commits}", "--").stdout
+        patch_ids = self._popen_git("patch-id", input=patches).stdout
+
+        patch_id_for_commit: Dict[FullCommitHash, FullPatchId] = {}
+        for line in patch_ids.splitlines():
+            patch_id, commit_hash = line.strip().split(" ", 1)
+            patch_id_for_commit[FullCommitHash.of(commit_hash)] = FullPatchId(patch_id)
+
+        return patch_id_for_commit
+
     def get_sole_remote_branch(self, branch: LocalBranchShortName) -> Optional[RemoteBranchShortName]:
         remote_branches = self.get_remote_branches()
         matching_remotes = [remote for remote in self.get_remotes() if (remote + "/" + branch) in remote_branches]
diff --git a/git_machete/github.py b/git_machete/github.py
index 373848e..3bd049d 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -253,7 +253,7 @@ class GitHubClient(CodeHostingClient):
                 if self.__token:
                     raise MacheteException(
                         first_line + f'Make sure that the GitHub API token provided by {self.__token.provider} '
-                                     f'is valid and allows for access to `{method.upper()}` `{url_prefix}{path}`.\n' + last_line)
+                        f'is valid and allows for access to `{method.upper()}` `{url_prefix}{path}`.\n' + last_line)
                 else:
                     raise MacheteException(
                         first_line + 'You might not have the required permissions for this repository.\n'
diff --git a/git_machete/gitlab.py b/git_machete/gitlab.py
index fb2622b..afb97ee 100644
--- a/git_machete/gitlab.py
+++ b/git_machete/gitlab.py
@@ -191,7 +191,7 @@ class GitLabClient(CodeHostingClient):
                 if self.__token:
                     raise MacheteException(
                         first_line + f'Make sure that the GitLab API token provided by {self.__token.provider} '
-                                     f'is valid and allows for access to `{method.upper()}` `{url_prefix}{path}`.\n' + last_line)
+                        f'is valid and allows for access to `{method.upper()}` `{url_prefix}{path}`.\n' + last_line)
                 else:
                     raise MacheteException(
                         first_line + 'You might not have the required permissions for this project.\n'
diff --git a/git_machete/options.py b/git_machete/options.py
index 3895b8d..2d4d593 100644
--- a/git_machete/options.py
+++ b/git_machete/options.py
@@ -21,7 +21,8 @@ class CommandLineOptions:
         self.opt_list_commits_with_hashes: bool = False
         self.opt_merge: bool = False
         self.opt_n: bool = False
-        self.opt_no_detect_squash_merges: bool = False
+        self.opt_squash_merge_detection_string: str = "simple"
+        self.opt_squash_merge_detection_origin: Optional[str] = None
         self.opt_no_edit_merge: bool = False
         self.opt_no_interactive_rebase: bool = False
         self.opt_onto: Optional[LocalBranchShortName] = None
diff --git a/git_machete/utils.py b/git_machete/utils.py
index 12b9d1f..af0f430 100644
--- a/git_machete/utils.py
+++ b/git_machete/utils.py
@@ -187,10 +187,13 @@ class PopenResult(NamedTuple):
 
 
 def _popen_cmd(cmd: str, *args: str,
-               cwd: Optional[str] = None, env: Optional[Dict[str, str]] = None) -> PopenResult:
+               cwd: Optional[str] = None, env: Optional[Dict[str, str]] = None, input: Optional[str] = None) -> PopenResult:
+    stdin = subprocess.PIPE if input is not None else None
+    input_bytes = input.encode('utf-8') if input else None
+
     # capture_output argument is only supported since Python 3.7
-    process = subprocess.Popen([cmd] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env)
-    stdout_bytes, stderr_bytes = process.communicate()
+    process = subprocess.Popen([cmd] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=stdin, cwd=cwd, env=env)
+    stdout_bytes, stderr_bytes = process.communicate(input_bytes)
     exit_code: int = process.returncode  # must be retrieved after process.communicate()
     stdout: str = stdout_bytes.decode('utf-8')
     stderr: str = stderr_bytes.decode('utf-8')
@@ -198,7 +201,7 @@ def _popen_cmd(cmd: str, *args: str,
 
 
 def popen_cmd(cmd: str, *args: str, cwd: Optional[str] = None,
-              env: Optional[Dict[str, str]] = None, hide_debug_output: bool = False) -> PopenResult:
+              env: Optional[Dict[str, str]] = None, hide_debug_output: bool = False, input: Optional[str] = None) -> PopenResult:
     chdir_upwards_until_current_directory_exists()
 
     flat_cmd = get_cmd_shell_repr(cmd, *args, env=env)
@@ -215,7 +218,7 @@ def popen_cmd(cmd: str, *args: str, cwd: Optional[str] = None,
         print_command(flat_cmd)
 
     start = time.time()
-    exit_code, stdout, stderr = result = _popen_cmd(cmd, *args, cwd=cwd, env=env)
+    exit_code, stdout, stderr = result = _popen_cmd(cmd, *args, cwd=cwd, env=env, input=input)
     if measure_command_time:  # pragma: no cover
         end = time.time()
         elapsed_ms = int((end - start) * 1e3)
diff --git a/tox.ini b/tox.ini
index ec5f983..2aac284 100644
--- a/tox.ini
+++ b/tox.ini
@@ -137,44 +137,52 @@ commands =
 [testenv:sphinx-html]
 basepython=3.11
 description = "Build Sphinx documentation in HTML"
-allowlist_externals = bash
 deps =
   -r{[requirements]dir}/sphinx-docs.txt
+allowlist_externals = bash
 commands =
   bash docs/generate-sphinx-html.sh docs/html
 
 [testenv:sphinx-man]
 basepython=3.11
 description = "Build Sphinx documentation in groff format (Unix man page)"
-allowlist_externals = bash
 deps =
   -r{[requirements]dir}/sphinx-docs.txt
+allowlist_externals = bash
 commands =
   bash docs/generate-sphinx-man.sh docs/man
 
+[testenv:sphinx-man-display]
+basepython=3.11
+description = "Build and display Sphinx documentation in groff format (Unix man page)"
+depends = sphinx-man
+allowlist_externals = sh
+commands =
+  sh -c 'groff -man -Tascii < docs/man/git-machete.1 2>/dev/null'
+
 [testenv:sphinx-man-check]
 basepython=3.11
 description = "Check if Unix man page is up to date with reStructuredText sources"
-allowlist_externals = bash
 deps =
   -r{[requirements]dir}/sphinx-docs.txt
+allowlist_externals = bash
 commands =
   bash docs/enforce-sphinx-man-up-to-date.sh
 
 [testenv:py-docs]
 description = "Build Python documentation"
 # The generation of the python docs uses git_machete package
-allowlist_externals = bash
 deps =
   -r{[requirements]dir}/py-docs.txt
+allowlist_externals = sh
 commands =
-  bash -c "python docs/generate_py_docs.py > git_machete/generated_docs.py"
+  sh -c "python docs/generate_py_docs.py > git_machete/generated_docs.py"
 
 [testenv:py-docs-check]
 description = "Check if Python documentation is up to date with with reStructuredText sources"
-allowlist_externals = bash
 deps =
   -r{[requirements]dir}/py-docs.txt
+allowlist_externals = bash
 commands =
   bash docs/enforce-py-docs-up-to-date.sh
 


Test patch:
diff --git a/tests/test_git_operations.py b/tests/test_git_operations.py
index 8f9fe28..cbe266c 100644
--- a/tests/test_git_operations.py
+++ b/tests/test_git_operations.py
@@ -45,3 +45,122 @@ class TestGitOperations(BaseTest):
                                                       later=LocalBranchShortName('master')) is False
         assert self.repo_sandbox.is_ancestor_or_equal(earlier=LocalBranchShortName('develop'),
                                                       later=LocalBranchShortName('feature')) is True
+
+    def test_is_equivalent_tree_or_patch_reachable_with_squash_merge(self) -> None:
+        (
+            self.repo_sandbox.new_branch("master")
+                .commit("master first commit")
+                .new_branch("feature")
+                .commit("feature commit")
+                .check_out("master")
+        )
+
+        git = GitContext()
+        assert git._run_git("merge", "--squash", "feature", flush_caches=False) == 0
+        assert git._run_git("commit", "-m", "squashed", flush_caches=False) == 0
+
+        feature = AnyRevision("feature")
+        master = AnyRevision("master")
+
+        # Both methods should return True, as there are no commits in master before we merged feature
+        assert git.is_equivalent_tree_reachable(equivalent_to=feature, reachable_from=master) is True
+        assert git.is_equivalent_patch_reachable(equivalent_to=feature, reachable_from=master) is True
+
+    def test_is_equivalent_tree_or_patch_reachable_with_squash_merge_and_commits_in_between(self) -> None:
+        (
+            self.repo_sandbox.new_branch("master")
+                .commit("master first commit")
+                .new_branch("feature")
+                .commit("feature commit")
+                .check_out("master")
+                .commit("extra commit")
+        )
+
+        git = GitContext()
+        assert git._run_git("merge", "--squash", "feature", flush_caches=False) == 0
+        assert git._run_git("commit", "-m", "squashed", flush_caches=False) == 0
+
+        feature = AnyRevision("feature")
+        master = AnyRevision("master")
+
+        # Here the simple method will not detect the squash merge, as there are commits in master before we merged feature so
+        # there's no tree hash in master that matches the tree hash of feature
+        assert git.is_equivalent_tree_reachable(equivalent_to=feature, reachable_from=master) is False
+        assert git.is_equivalent_patch_reachable(equivalent_to=feature, reachable_from=master) is True
+
+        self.repo_sandbox.check_out("master").commit("another master commit")
+        git.flush_caches()  # so that the old position of `master` isn't remembered
+
+        assert git.is_equivalent_tree_reachable(equivalent_to=feature, reachable_from=master) is False
+        assert git.is_equivalent_patch_reachable(equivalent_to=feature, reachable_from=master) is True
+
+    def test_is_equivalent_tree_or_patch_reachable_with_rebase(self) -> None:
+        (
+            self.repo_sandbox.new_branch("master")
+                .commit("master first commit")
+                .new_branch("feature")
+                .commit("feature commit")
+                .check_out("master")
+        )
+
+        git = GitContext()
+        assert git._run_git("rebase", "feature", flush_caches=False) == 0
+
+        feature = AnyRevision("feature")
+        master = AnyRevision("master")
+
+        # Same as merge example, both methods should return True as there are no commits in master before we rebased feature
+        assert git.is_equivalent_tree_reachable(equivalent_to=feature, reachable_from=master) is True
+        assert git.is_equivalent_patch_reachable(equivalent_to=feature, reachable_from=master) is True
+
+        self.repo_sandbox.check_out("master").commit("another master commit")
+        git.flush_caches()  # so that the old position of `master` isn't remembered
+
+        # Simple method fails if there are commits after the rebase, as this case is covered by the "is ancestor"
+        # check in the is_merged_to method in client.py
+        assert git.is_equivalent_tree_reachable(equivalent_to=feature, reachable_from=master) is False
+        assert git.is_equivalent_patch_reachable(equivalent_to=feature, reachable_from=master) is True
+
+    def test_is_equivalent_tree_or_patch_reachable_with_rebase_and_commits_in_between(self) -> None:
+        (
+            self.repo_sandbox.new_branch("master")
+                .commit("master first commit")
+                .new_branch("feature")
+                .commit("feature commit")
+                .check_out("master")
+                .commit("extra commit")
+        )
+
+        git = GitContext()
+        assert git._run_git("rebase", "feature", flush_caches=False) == 0
+
+        feature = AnyRevision("feature")
+        master = AnyRevision("master")
+
+        # Same as the merge example, the simple method will not detect the rebase, as there are commits
+        # in master before we rebased feature and tree hashes are always different
+        assert git.is_equivalent_tree_reachable(equivalent_to=feature, reachable_from=master) is False
+        assert git.is_equivalent_patch_reachable(equivalent_to=feature, reachable_from=master) is True
+
+        self.repo_sandbox.check_out("master").commit("another master commit")
+        git.flush_caches()  # so that the old position of `master` isn't remembered
+
+        assert git.is_equivalent_tree_reachable(equivalent_to=feature, reachable_from=master) is False
+        assert git.is_equivalent_patch_reachable(equivalent_to=feature, reachable_from=master) is True
+        # To cover retrieval of the result from cache
+        assert git.is_equivalent_patch_reachable(equivalent_to=feature, reachable_from=master) is True
+
+    def test_is_equivalent_tree_or_patch_reachable_when_no_common_ancestor(self) -> None:
+        (
+            self.repo_sandbox.new_branch("master")
+            .commit("master first commit")
+            .new_orphan_branch("feature")
+            .commit("feature commit")
+        )
+
+        git = GitContext()
+        feature = AnyRevision("feature")
+        master = AnyRevision("master")
+
+        assert git.is_equivalent_tree_reachable(equivalent_to=feature, reachable_from=master) is False
+        assert git.is_equivalent_patch_reachable(equivalent_to=feature, reachable_from=master) is False
diff --git a/tests/test_help.py b/tests/test_help.py
index 92373ba..1eb7c68 100644
--- a/tests/test_help.py
+++ b/tests/test_help.py
@@ -16,7 +16,7 @@ class TestHelp(BaseTest):
             launch_command()
         assert ExitCode.ARGUMENT_ERROR == e.value.code
 
-        launch_command("help")
+        assert "--verbose" in launch_command("help")
 
         with pytest.raises(SystemExit) as e:
             launch_command("help", "no-such-command")
@@ -30,7 +30,7 @@ class TestHelp(BaseTest):
 
             if command not in help_topics:
                 with pytest.raises(SystemExit) as e:
-                    launch_command(command, "--help")
+                    assert "Usage:" in launch_command(command, "--help")
                 assert ExitCode.SUCCESS == e.value.code
             else:
                 with pytest.raises(SystemExit) as e:
@@ -41,3 +41,8 @@ class TestHelp(BaseTest):
         for command in commands_and_aliases:
             help_output = launch_command('help', command)
             assert '\033' not in help_output
+
+    def test_help_succeeds_despite_invalid_git_config_key(self) -> None:
+        self.repo_sandbox.set_git_config_key("machete.squashMergeDetection", "invalid")
+        help_output = launch_command('help')
+        assert "Usage:" in help_output
diff --git a/tests/test_status.py b/tests/test_status.py
index 70788c4..1c7f2fc 100644
--- a/tests/test_status.py
+++ b/tests/test_status.py
@@ -306,7 +306,7 @@ class TestStatus(BaseTest):
         )
         assert_success(['status'], expected_status_output)
 
-    def test_squashed_branch_recognized_as_merged_with_traverse(self) -> None:
+    def test_status_squashed_branch_recognized_as_merged_with_traverse(self) -> None:
 
         (
             self.repo_sandbox.new_branch("root")
@@ -378,10 +378,8 @@ class TestStatus(BaseTest):
             """,
         )
 
-        # but under --no-detect-squash-merges, feature is detected as "x" (behind) develop
-        assert_success(
-            ["status", "-l", "--no-detect-squash-merges"],
-            """
+        # under --squash-merge-detection=none, feature is detected as "x" (out of sync) with develop
+        expected_output_detection_none = """
             root
             |
             | develop
@@ -395,8 +393,27 @@ class TestStatus(BaseTest):
                 | child_1
                 | child_2
                 o-child *
-            """,
+            """
+        assert_success(
+            ["status", "-l", "--no-detect-squash-merges"],
+            "          Warn: --no-detect-squash-merges is deprecated, "
+            "use --squash-merge-detection=none instead\n" + expected_output_detection_none
+        )
+        assert_success(
+            ["status", "-l", "--squash-merge-detection=none"],
+            expected_output_detection_none
+        )
+        self.repo_sandbox.set_git_config_key('machete.squashMergeDetection', 'none')
+        assert_success(
+            ["status", "-l"],
+            expected_output_detection_none
         )
+        self.repo_sandbox.set_git_config_key('machete.squashMergeDetection', 'lolxd')
+        assert_failure(
+            ["status", "-l"],
+            "Invalid value for machete.squashMergeDetection git config key: lolxd. Valid values are none, simple, exact"
+        )
+        self.repo_sandbox.unset_git_config_key('machete.squashMergeDetection')
 
         # traverse then slide out the feature branch
         launch_command("traverse", "-w", "-y")
@@ -458,6 +475,55 @@ class TestStatus(BaseTest):
             """,
         )
 
+    def test_status_for_squash_merge_and_commits_in_between(self) -> None:
+        (
+            self.repo_sandbox
+            .new_branch("master")
+            .remove_remote("origin")
+            .commit("master first commit")
+            .new_branch("feature")
+            .commit("feature commit")
+            .check_out("master")
+            .commit("extra commit")
+            .execute("git merge --squash feature")
+            .execute("git commit -m squashed")
+        )
+
+        body: str = \
+            """
+            master
+                feature
+            """
+        rewrite_branch_layout_file(body)
+
+        # Here the simple method will not detect the squash merge, as there are commits in master before we merged feature so
+        # there's no tree hash in master that matches the tree hash of feature
+        expected_status_output_simple = (
+            """
+            master *
+            |
+            x-feature
+            """
+        )
+        assert_success(['status'], expected_status_output_simple)
+        assert_success(['status', '--squash-merge-detection=simple'], expected_status_output_simple)
+        expected_status_output_exact = (
+            """
+            master *
+            |
+            m-feature
+            """
+        )
+        assert_success(['status', '--squash-merge-detection=exact'], expected_status_output_exact)
+        self.repo_sandbox.set_git_config_key('machete.squashMergeDetection', 'exact')
+        assert_success(['status'], expected_status_output_exact)
+
+    def test_status_invalid_squash_merge_detection(self) -> None:
+        assert_failure(["status", "--squash-merge-detection=invalid"],
+                       "Invalid value for --squash-merge-detection flag: invalid. Valid values are none, simple, exact")
+        assert_failure(["status", "--squash-merge-detection=none", "--squash-merge-detection=invalid"],
+                       "Invalid value for --squash-merge-detection flag: invalid. Valid values are none, simple, exact")
+
     def test_status_inferring_counterpart_for_fetching_of_branch(self) -> None:
         origin_1_remote_path = self.repo_sandbox.create_repo("remote-1", bare=True)
         (
diff --git a/tests/test_traverse.py b/tests/test_traverse.py
index f8b365c..c573e03 100644
--- a/tests/test_traverse.py
+++ b/tests/test_traverse.py
@@ -986,11 +986,12 @@ class TestTraverse(BaseTest):
         assert_failure(["traverse"], expected_error_message)
 
     def test_traverse_invalid_flag_values(self) -> None:
-        self.setup_standard_tree()
-        assert_failure(["traverse", "--start-from=nowhere"],
-                       "Invalid argument for --start-from. Valid arguments: here|root|first-root.")
         assert_failure(["traverse", "--return-to=dunno-where"],
-                       "Invalid argument for --return-to. Valid arguments: here|nearest-remaining|stay.")
+                       "Invalid value for --return-to flag: dunno-where. Valid values are here, nearest-remaining, stay")
+        assert_failure(["traverse", "--squash-merge-detection=lolxd"],
+                       "Invalid value for --squash-merge-detection flag: lolxd. Valid values are none, simple, exact")
+        assert_failure(["traverse", "--start-from=nowhere"],
+                       "Invalid value for --start-from flag: nowhere. Valid values are here, root, first-root")
 
     def test_traverse_removes_current_directory(self) -> None:
         (