Repository: VirtusLab/git-machete
Pull request #1311 (2024-08-24 11:39:00)

Release v3.29.0


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ddc7b4a..9e3e53f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -32,9 +32,14 @@ Install `fish` and `shellcheck` for `run-all-checks.sh` to pass successfully.
 
 ## Run tests locally
 
-To run tests in terminal, it is required to have at least one Python interpreter version installed with `tox`, available versions are 3.{6-11}.
-To run all tests, execute `tox` (requires each Python 3.X version previously specified), to run selected test environment execute `tox -e test_env_name` ---
-the name of the `test_env_name` can specify Python version of the environment, e.g. `py37`, if the `tox` can't discover version from the name, the highest version currently installed will be used.
+```shell
+tox -e py
+```
+
+To display full operands in failed assertions (rather than just diffs), use
+```shell
+tox -e py -- -k github --full-operands
+```
 
 ## Install locally for development purposes
 
diff --git a/README.md b/README.md
index 55e7e53..8f09468 100644
--- a/README.md
+++ b/README.md
@@ -127,8 +127,6 @@ nix-env -i git-machete
 The latest released version, however, is generally available in the unstable channel.
 Stable channels may lag behind; see [repology](https://repology.org/project/git-machete/versions) for the current channel-package mapping.
 
-<br/>
-
 ### Using Pex
 
 The [Pex tool](https://github.com/pex-tool/pex) (short for Python EXecutable) allows you to build "pex" files which are executable Python environments in a single file.
@@ -141,6 +139,8 @@ pex git-machete -m git_machete.bin:main -o git-machete
 
 Then put the produced `git-machete` file somewhere on your `PATH`.
 
+<br/>
+
 ## Quick start
 
 ### Discover the branch layout
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 8339afb..7f43e8d 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,12 @@
 # Release notes
 
+## New in git-machete 3.29.0
+
+- added: git config keys `machete.github.prDescriptionIntroStyle` and `machete.gitlab.mrDescriptionIntroStyle`
+- added: ability to turn off PR/MR description intro completely by setting the git config key to `none` (suggested by @tir38)
+- added: ability to include downstream PRs/MRs in PR/MR description intro by setting the git config key to `full` (suggested by @aouaki)
+- changed: layout and ordering of PRs/MRs in PR/MR description intro to better match `git machete status`
+
 ## New in git-machete 3.28.0
 
 - added: ability to specify pass-through flags in `diff` and `log`, for example `git machete diff -- file.txt`, `git machete log -- --patch` (partly contributed by @tdyas)
diff --git a/ci/checks/enforce-consistent-style-for-fork-point.sh b/ci/checks/enforce-consistent-style-for-fork-point.sh
index b2c3c93..3967d03 100755
--- a/ci/checks/enforce-consistent-style-for-fork-point.sh
+++ b/ci/checks/enforce-consistent-style-for-fork-point.sh
@@ -2,10 +2,9 @@
 
 set -e -o pipefail -u
 
-self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
 self_name=$(basename "$0")
-source "$self_dir"/utils.sh
 
 if git grep -n '[fF]orkpoint' -- :!**/$self_name; then
-  die "Please use 'fork point' or 'Fork point' or 'fork_point' instead of 'forkpoint' or 'Forkpoint'."
+  echo "Please use 'fork point' or 'Fork point' or 'fork_point' instead of 'forkpoint' or 'Forkpoint'."
+  exit 1
 fi
diff --git a/ci/checks/enforce-consistent-style-for-github.sh b/ci/checks/enforce-consistent-style-for-github.sh
index f09cad7..ace4d13 100755
--- a/ci/checks/enforce-consistent-style-for-github.sh
+++ b/ci/checks/enforce-consistent-style-for-github.sh
@@ -2,10 +2,9 @@
 
 set -e -o pipefail -u
 
-self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-source "$self_dir"/utils.sh
 self_name=$(basename "$0")
 
 if git grep -n Github -- :!**/$self_name; then
-  die "Please use 'GitHub' instead of 'Github'."
+  echo "Please use 'GitHub' instead of 'Github'."
+  exit 1
 fi
diff --git a/ci/checks/enforce-consistent-style-for-gitlab.sh b/ci/checks/enforce-consistent-style-for-gitlab.sh
index 230b8ad..257ca10 100755
--- a/ci/checks/enforce-consistent-style-for-gitlab.sh
+++ b/ci/checks/enforce-consistent-style-for-gitlab.sh
@@ -2,10 +2,9 @@
 
 set -e -o pipefail -u
 
-self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-source "$self_dir"/utils.sh
 self_name=$(basename "$0")
 
 if git grep -n Gitlab -- :!**/$self_name; then
-  die "Please use 'GitLab' instead of 'Gitlab'."
+  echo "Please use 'GitLab' instead of 'Gitlab'."
+  exit 1
 fi
diff --git a/ci/checks/enforce-issue-number-for-todos.sh b/ci/checks/enforce-issue-number-for-todos.sh
index fb84996..2e49fc1 100755
--- a/ci/checks/enforce-issue-number-for-todos.sh
+++ b/ci/checks/enforce-issue-number-for-todos.sh
@@ -2,10 +2,8 @@
 
 set -e -o pipefail -u
 
-self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-source "$self_dir"/utils.sh
-
 # Negative lookahead (?!...), requires Perl syntax (-P): find fixmes/todos NOT followed by issue number
 if git grep --perl-regexp -I --ignore-case --line-number '(\*|//|#|<!--)\s*(fixme|todo)(?! \([[:alnum:]/-]*#[0-9]+\): )'; then
-  die "Use 'TODO|FIXME (<other-repo-name-or-empty>#<issue-number>): <short-description>' format for the above TODOs and FIXMEs"
+  echo "Use 'TODO|FIXME (<other-repo-name-or-empty>#<issue-number>): <short-description>' format for the above TODOs and FIXMEs"
+  exit 1
 fi
diff --git a/ci/checks/enforce-mocking-only-whitelisted-methods.sh b/ci/checks/enforce-mocking-only-whitelisted-methods.sh
index af0bbe9..6997512 100755
--- a/ci/checks/enforce-mocking-only-whitelisted-methods.sh
+++ b/ci/checks/enforce-mocking-only-whitelisted-methods.sh
@@ -2,8 +2,6 @@
 
 set -e -o pipefail -u
 
-self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-source "$self_dir"/utils.sh
 self_name=$(basename "$0")
 
 whitelisted_methods="\
@@ -30,6 +28,7 @@ actual_methods=$(git grep -Pho "(?<=self\.patch_symbol\(mocker, ['\"]).*?(?=['\"
 # `grep ''` to check if the output is non-empty (true if non-empty, false if empty)
 if comm -13 <(echo "$whitelisted_methods") <(echo "$actual_methods") | grep ''; then
   echo
-  error "The above properties/methods are mocked in at least one test method, but are NOT whitelisted in $self_name."
-  die   "While it is possible to add new whitelisted properties/methods, consider redesigning the test(s) first."
+  echo "The above properties/methods are mocked in at least one test method, but are NOT whitelisted in $self_name."
+  echo "While it is possible to add new whitelisted properties/methods, consider redesigning the test(s) first."
+  exit 1
 fi
diff --git a/ci/checks/enforce-newline-at-eof.sh b/ci/checks/enforce-newline-at-eof.sh
index ca743d6..02915f8 100755
--- a/ci/checks/enforce-newline-at-eof.sh
+++ b/ci/checks/enforce-newline-at-eof.sh
@@ -2,9 +2,6 @@
 
 set -e -o pipefail -u
 
-self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-source "$self_dir"/utils.sh
-
 found=0
 for file in $(git grep -I --files-with-matches ''); do
   last_two_characters=$(tail -c2 "$file" | xxd -p)
@@ -18,5 +15,6 @@ for file in $(git grep -I --files-with-matches ''); do
 done
 
 if [[ $found -ne 0 ]]; then
-  die 'The above non-binary file(s) do not end with a single newline character, please tidy up'
+  echo 'The above non-binary file(s) do not end with a single newline character, please tidy up'
+  exit 1
 fi
diff --git a/ci/checks/prohibit-exempli-gratia-in-rst.sh b/ci/checks/prohibit-exempli-gratia-in-rst.sh
index 438edb4..3fe6e6b 100755
--- a/ci/checks/prohibit-exempli-gratia-in-rst.sh
+++ b/ci/checks/prohibit-exempli-gratia-in-rst.sh
@@ -2,9 +2,7 @@
 
 set -e -o pipefail -u
 
-self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-source "$self_dir"/utils.sh
-
 if git grep -En 'e\. ?g\.' -- '*.rst'; then
-  die 'Do not use `e.g.` in docs; use a clearer alternative like `for example` instead'
+  echo 'Do not use `e.g.` in docs; use a clearer alternative like `for example` instead'
+  exit 1
 fi
diff --git a/ci/checks/prohibit-github-in-gitlab-files.sh b/ci/checks/prohibit-github-in-gitlab-files.sh
new file mode 100755
index 0000000..6932959
--- /dev/null
+++ b/ci/checks/prohibit-github-in-gitlab-files.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+if git grep -E -i -n -e 'github' --and --not -e 'report this error|valid GitLab project' -- '*gitlab*' ':!**/prohibit-*-in-*-files.sh'; then
+  echo
+  echo 'Stray usage of `github` in GitLab-related file(s), is it a copy-paste error?'
+  exit 1
+fi
diff --git a/ci/checks/prohibit-gitlab-in-github-files.sh b/ci/checks/prohibit-gitlab-in-github-files.sh
new file mode 100755
index 0000000..91a648f
--- /dev/null
+++ b/ci/checks/prohibit-gitlab-in-github-files.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+if git grep -E -i -n -e 'gitlab' --and --not -e 'valid GitHub repo' -- '*github*' ':!**/prohibit-*-in-*-files.sh'; then
+  echo
+  echo 'Stray usage of `gitlab` in GitHub-related file(s), is it a copy-paste error?'
+  exit 1
+fi
diff --git a/ci/checks/prohibit-id-est-in-rst.sh b/ci/checks/prohibit-id-est-in-rst.sh
index a22cc0e..ee2f772 100755
--- a/ci/checks/prohibit-id-est-in-rst.sh
+++ b/ci/checks/prohibit-id-est-in-rst.sh
@@ -2,9 +2,7 @@
 
 set -e -o pipefail -u
 
-self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-source "$self_dir"/utils.sh
-
 if git grep -En 'i\. ?e\.' -- '*.rst'; then
-  die 'Do not use `i.e.` in docs; use a clearer alternative like a long dash `---` instead'
+  echo 'Do not use `i.e.` in docs; use a clearer alternative like a long dash `---` instead'
+  exit 1
 fi
diff --git a/ci/checks/prohibit-mrs-in-github-files.sh b/ci/checks/prohibit-mrs-in-github-files.sh
new file mode 100755
index 0000000..872e027
--- /dev/null
+++ b/ci/checks/prohibit-mrs-in-github-files.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+self_name=$(basename "$0")
+
+if git grep -EIn 'MR|-mr|[mM]erge [rR]equest' -- '*github*' :!**/$self_name; then
+  echo
+  echo "GitHub uses *pull* requests rather than *merge* requests, please fix"
+  exit 1
+fi
diff --git a/ci/checks/prohibit-prs-in-gitlab-files.sh b/ci/checks/prohibit-prs-in-gitlab-files.sh
new file mode 100755
index 0000000..ec80f51
--- /dev/null
+++ b/ci/checks/prohibit-prs-in-gitlab-files.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+self_name=$(basename "$0")
+
+if git grep -EIn 'PR|-pr($|[^o])|[pP]ull [rR]equest' -- '*gitlab*' :!**/$self_name; then
+  echo
+  echo "GitLab uses *merge* requests rather than *pull* requests, please fix"
+  echo 'Usage of `pull_request` in GitLab-related identifiers is allowed so that we avoid inventing a common abstract word for pull and merge requests'
+  exit 1
+fi
diff --git a/ci/checks/prohibit-split-backslash-n.sh b/ci/checks/prohibit-split-backslash-n.sh
index d4655cd..b908817 100755
--- a/ci/checks/prohibit-split-backslash-n.sh
+++ b/ci/checks/prohibit-split-backslash-n.sh
@@ -2,9 +2,8 @@
 
 set -e -o pipefail -u
 
-self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
-source "$self_dir"/utils.sh
-
 if git grep -n 'split(.\\n.)' -- '*.py'; then
-  die 'Do not use split("\\n") on strings as it does not cover \\r\\n line breaks; use splitlines() instead'
+  # shellcheck disable=SC2028
+  echo 'Do not use split("\\n") on strings as it does not cover \\r\\n line breaks; use splitlines() instead'
+  exit 1
 fi
diff --git a/ci/checks/prohibit-trailing-whitespace.sh b/ci/checks/prohibit-trailing-whitespace.sh
index 4504d42..99c49ef 100755
--- a/ci/checks/prohibit-trailing-whitespace.sh
+++ b/ci/checks/prohibit-trailing-whitespace.sh
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-if git grep -EIn ' +$' -- :!'*.py'; then
+if git grep -EIn ' +$' -- :!git_machete/generated_docs.py; then
   echo 'The above lines contain trailing whitespace, please tidy up'
   exit 1
 fi
diff --git a/ci/checks/run-all-checks.sh b/ci/checks/run-all-checks.sh
index e847130..00dd902 100755
--- a/ci/checks/run-all-checks.sh
+++ b/ci/checks/run-all-checks.sh
@@ -31,7 +31,11 @@ prohibit-exempli-gratia-in-rst.sh
 prohibit-fork-point-in-git-context.sh
 prohibit-id-est-in-rst.sh
 prohibit-double-backticks-in-python.sh
+prohibit-github-in-gitlab-files.sh
+prohibit-gitlab-in-github-files.sh
 prohibit-markdown-links-in-rst.sh
+prohibit-mrs-in-github-files.sh
+prohibit-prs-in-gitlab-files.sh
 prohibit-single-backtick-in-rst.sh
 prohibit-split-backslash-n.sh
 prohibit-strings-split-without-delimiter.sh
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index ed1d172..3da9edc 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" "Aug 17, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Aug 23, 2024" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.28.0
+git-machete \- git-machete 3.29.0
 .sp
 git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
 .sp
@@ -360,6 +360,20 @@ but also \fBgithub checkout\-prs\fP, \fBgithub create\-pr\fP, \fBgithub retarget
 Setting this config key to \fBtrue\fP will force \fBgit machete github create\-pr\fP to take PR description
 from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.github/pull_request_template.md\fP is present.
 .TP
+.B \fBmachete.github.prDescriptionIntroStyle\fP:
+.INDENT 7.0
+.TP
+.B Select the style of the intro prepended to PR description:
+.INDENT 7.0
+.IP \(bu 2
+\fBfull\fP    \-\-\- include both a chain of upstream PRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream PRs
+.IP \(bu 2
+\fBup\-only\fP \-\-\- default, include only a chain of upstream PRs
+.IP \(bu 2
+\fBnone\fP    \-\-\- prepend no intro to the PR description at all
+.UNINDENT
+.UNINDENT
+.TP
 .B \fBmachete.gitlab.{domain,remote,namespace,project}\fP:
 .INDENT 7.0
 .INDENT 3.5
@@ -392,6 +406,20 @@ but also \fBgitlab checkout\-mrs\fP, \fBgitlab create\-mr\fP, \fBgitlab retarget
 Setting this config key to \fBtrue\fP will force \fBgit machete gitlab create\-mr\fP to take MR description
 from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.gitlab/merge_request_templates/Default.md\fP is present.
 .TP
+.B \fBmachete.gitlab.mrDescriptionIntroStyle\fP:
+.INDENT 7.0
+.TP
+.B Select the style of the intro prepended to MR description:
+.INDENT 7.0
+.IP \(bu 2
+\fBfull\fP    \-\-\- include both a chain of upstream MRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream MRs
+.IP \(bu 2
+\fBup\-only\fP \-\-\- default, include only a chain of upstream MRs
+.IP \(bu 2
+\fBnone\fP    \-\-\- prepend no intro to the MR description at all
+.UNINDENT
+.UNINDENT
+.TP
 .B \fBmachete.overrideForkPoint.<branch>.to\fP:
 Executing \fBgit machete fork\-point \-\-override\-to[\-parent|\-inferred|=<revision>] [<branch>]\fP sets up a fork point override for \fB<branch>\fP\&.
 .sp
@@ -1009,10 +1037,10 @@ Draft PRs don\(aqt get code owners automatically added as reviewers.
 .B \fBretarget\-pr [\-b|\-\-branch=<branch>] [\-\-ignore\-if\-missing]\fP:
 Sets the base of the current (or specified) branch\(aqs PR to upstream (parent) branch, as seen by git machete (see \fBgit machete show up\fP).
 .sp
-If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended with a section
-listing the entire related chain of PRs.
+If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended
+with an intro section listing the entire related chain of PRs.
 .sp
-This header will be updated or removed accordingly with the subsequent runs of \fBretarget\-pr\fP\&.
+This intro will be updated or removed accordingly with the subsequent runs of \fBretarget\-pr\fP, even if the base branch is already up to date.
 .sp
 \fBOptions:\fP
 .INDENT 7.0
@@ -1075,6 +1103,20 @@ but also \fBgithub checkout\-prs\fP, \fBgithub create\-pr\fP, \fBgithub retarget
 .B \fBmachete.github.forceDescriptionFromCommitMessage\fP (\fBcreate\-pr\fP only):
 Setting this config key to \fBtrue\fP will force \fBgit machete github create\-pr\fP to take PR description
 from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.github/pull_request_template.md\fP is present.
+.TP
+.B \fBmachete.github.prDescriptionIntroStyle\fP (\fBcreate\-pr\fP, \fBrestack\-pr\fP and \fBretarget\-pr\fP):
+.INDENT 7.0
+.TP
+.B Select the style of the intro prepended to PR description:
+.INDENT 7.0
+.IP \(bu 2
+\fBfull\fP    \-\-\- include both a chain of upstream PRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream PRs
+.IP \(bu 2
+\fBup\-only\fP \-\-\- default, include only a chain of upstream PRs
+.IP \(bu 2
+\fBnone\fP    \-\-\- prepend no intro to the PR description at all
+.UNINDENT
+.UNINDENT
 .UNINDENT
 .sp
 \fBEnvironment variables (all subcommands):\fP
@@ -1102,7 +1144,7 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 \fBNOTE:\fP
 .INDENT 0.0
 .INDENT 3.5
-To allow GitLab API access for private repositories (and also to perform side\-effecting actions like opening a PR,
+To allow GitLab API access for private projects (and also to perform side\-effecting actions like opening a MR,
 even in case of public projects), a GitLab API token with \fBapi\fP scope is required, see \X'tty: link https://gitlab.com/-/user_settings/personal_access_tokens'\fI\%https://gitlab.com/\-/user_settings/personal_access_tokens\fP\X'tty: link'\&.
 This will be resolved from the first of:
 .INDENT 0.0
@@ -1160,9 +1202,9 @@ Also include full MR URLs in the annotations (rather than just MR number).
 .UNINDENT
 .TP
 .B \fBcheckout\-mrs [\-\-all | \-\-by=<gitlab\-login> | \-\-mine | <MR\-number\-1> ... <MR\-number\-N>]\fP:
-Check out the source branch of the given pull requests (specified by numbers or by a flag),
-also traverse chain of pull requests upwards, adding branches one by one to git\-machete and check them out locally.
-Once the specified pull requests are checked out locally, annotate local branches with corresponding pull request numbers.
+Check out the source branch of the given merge requests (specified by numbers or by a flag),
+also traverse chain of merge requests upwards, adding branches one by one to git\-machete and check them out locally.
+Once the specified merge requests are checked out locally, annotate local branches with corresponding merge request numbers.
 If only one MR has been checked out, then switch the local repository\(aqs HEAD to its source branch.
 .sp
 When the current user is NOT the author of the MR associated with that branch, adds \fBrebase=no push=no\fP branch qualifiers used by \fBgit machete traverse\fP,
@@ -1183,7 +1225,7 @@ Checkout open MRs for the current user associated with the GitLab token.
 .sp
 \fBParameters:\fP
 .sp
-\fB<MR\-number\-1> ... <MR\-number\-N>\fP    Pull request numbers to checkout.
+\fB<MR\-number\-1> ... <MR\-number\-N>\fP    Merge request numbers to checkout.
 .TP
 .B \fBcreate\-mr [\-\-draft] [\-\-title=<title>] [\-\-yes]\fP:
 Creates a MR for the current branch, using the upstream (parent) branch as the MR source branch.
@@ -1233,10 +1275,10 @@ Draft MRs don\(aqt get code owners automatically added as reviewers.
 .B \fBretarget\-mr [\-b|\-\-branch=<branch>] [\-\-ignore\-if\-missing]\fP:
 Sets the target of the current (or specified) branch\(aqs MR to upstream (parent) branch, as seen by git machete (see \fBgit machete show up\fP).
 .sp
-If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended with a section
-listing the entire related chain of MRs.
+If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended
+with an intro section listing the entire related chain of MRs.
 .sp
-This header will be updated or removed accordingly with the subsequent runs of \fBretarget\-mr\fP\&.
+This intro will be updated or removed accordingly with the subsequent runs of \fBretarget\-mr\fP, even if the target branch is already up to date.
 .sp
 \fBOptions:\fP
 .INDENT 7.0
@@ -1285,6 +1327,20 @@ but also \fBgitlab checkout\-mrs\fP, \fBgitlab create\-mr\fP, \fBgitlab retarget
 .B \fBmachete.gitlab.forceDescriptionFromCommitMessage\fP (\fBcreate\-mr\fP only):
 Setting this config key to \fBtrue\fP will force \fBgit machete gitlab create\-mr\fP to take MR description
 from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.gitlab/merge_request_templates/Default.md\fP is present.
+.TP
+.B \fBmachete.gitlab.mrDescriptionIntroStyle\fP (\fBcreate\-mr\fP, \fBrestack\-mr\fP and \fBretarget\-mr\fP):
+.INDENT 7.0
+.TP
+.B Select the style of the intro prepended to MR description:
+.INDENT 7.0
+.IP \(bu 2
+\fBfull\fP    \-\-\- include both a chain of upstream MRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream MRs
+.IP \(bu 2
+\fBup\-only\fP \-\-\- default, include only a chain of upstream MRs
+.IP \(bu 2
+\fBnone\fP    \-\-\- prepend no intro to the MR description at all
+.UNINDENT
+.UNINDENT
 .UNINDENT
 .sp
 \fBEnvironment variables (all subcommands):\fP
diff --git a/docs/source/cli/config.rst b/docs/source/cli/config.rst
index 6c3e93f..cf9b26b 100644
--- a/docs/source/cli/config.rst
+++ b/docs/source/cli/config.rst
@@ -18,6 +18,9 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
 ``machete.github.forceDescriptionFromCommitMessage``:
   .. include:: git-config-keys/github_forceDescriptionFromCommitMessage.rst
 
+``machete.github.prDescriptionIntroStyle``:
+  .. include:: git-config-keys/github_prDescriptionIntroStyle.rst
+
 ``machete.gitlab.{domain,remote,namespace,project}``:
   .. include:: git-config-keys/gitlab_access.rst
       :start-line: 3
@@ -28,6 +31,9 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
 ``machete.gitlab.forceDescriptionFromCommitMessage``:
   .. include:: git-config-keys/gitlab_forceDescriptionFromCommitMessage.rst
 
+``machete.gitlab.mrDescriptionIntroStyle``:
+  .. include:: git-config-keys/gitlab_mrDescriptionIntroStyle.rst
+
 ``machete.overrideForkPoint.<branch>.to``:
     Executing ``git machete fork-point --override-to[-parent|-inferred|=<revision>] [<branch>]`` sets up a fork point override for ``<branch>``.
 
diff --git a/docs/source/cli/github.rst b/docs/source/cli/github.rst
index f765937..272f8c3 100644
--- a/docs/source/cli/github.rst
+++ b/docs/source/cli/github.rst
@@ -112,10 +112,10 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
 ``retarget-pr [-b|--branch=<branch>] [--ignore-if-missing]``:
     Sets the base of the current (or specified) branch's PR to upstream (parent) branch, as seen by git machete (see ``git machete show up``).
 
-    If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended with a section
-    listing the entire related chain of PRs.
+    If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended
+    with an intro section listing the entire related chain of PRs.
 
-    This header will be updated or removed accordingly with the subsequent runs of ``retarget-pr``.
+    This intro will be updated or removed accordingly with the subsequent runs of ``retarget-pr``, even if the base branch is already up to date.
 
     **Options:**
 
@@ -144,6 +144,9 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
 ``machete.github.forceDescriptionFromCommitMessage`` (``create-pr`` only):
   .. include:: git-config-keys/github_forceDescriptionFromCommitMessage.rst
 
+``machete.github.prDescriptionIntroStyle`` (``create-pr``, ``restack-pr`` and ``retarget-pr``):
+  .. include:: git-config-keys/github_prDescriptionIntroStyle.rst
+
 **Environment variables (all subcommands):**
 
 ``GITHUB_TOKEN``
diff --git a/docs/source/cli/gitlab.rst b/docs/source/cli/gitlab.rst
index 8bb8df0..947444b 100644
--- a/docs/source/cli/gitlab.rst
+++ b/docs/source/cli/gitlab.rst
@@ -14,7 +14,7 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 
 .. note::
 
-    To allow GitLab API access for private repositories (and also to perform side-effecting actions like opening a PR,
+    To allow GitLab API access for private projects (and also to perform side-effecting actions like opening a MR,
     even in case of public projects), a GitLab API token with ``api`` scope is required, see https://gitlab.com/-/user_settings/personal_access_tokens.
     This will be resolved from the first of:
 
@@ -55,9 +55,9 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 
 
 ``checkout-mrs [--all | --by=<gitlab-login> | --mine | <MR-number-1> ... <MR-number-N>]``:
-    Check out the source branch of the given pull requests (specified by numbers or by a flag),
-    also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally.
-    Once the specified pull requests are checked out locally, annotate local branches with corresponding pull request numbers.
+    Check out the source branch of the given merge requests (specified by numbers or by a flag),
+    also traverse chain of merge requests upwards, adding branches one by one to git-machete and check them out locally.
+    Once the specified merge requests are checked out locally, annotate local branches with corresponding merge request numbers.
     If only one MR has been checked out, then switch the local repository's HEAD to its source branch.
 
     When the current user is NOT the author of the MR associated with that branch, adds ``rebase=no push=no`` branch qualifiers used by ``git machete traverse``,
@@ -73,7 +73,7 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 
     **Parameters:**
 
-    ``<MR-number-1> ... <MR-number-N>``    Pull request numbers to checkout.
+    ``<MR-number-1> ... <MR-number-N>``    Merge request numbers to checkout.
 
 ``create-mr [--draft] [--title=<title>] [--yes]``:
     Creates a MR for the current branch, using the upstream (parent) branch as the MR source branch.
@@ -113,10 +113,10 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 ``retarget-mr [-b|--branch=<branch>] [--ignore-if-missing]``:
     Sets the target of the current (or specified) branch's MR to upstream (parent) branch, as seen by git machete (see ``git machete show up``).
 
-    If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended with a section
-    listing the entire related chain of MRs.
+    If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended
+    with an intro section listing the entire related chain of MRs.
 
-    This header will be updated or removed accordingly with the subsequent runs of ``retarget-mr``.
+    This intro will be updated or removed accordingly with the subsequent runs of ``retarget-mr``, even if the target branch is already up to date.
 
     **Options:**
 
@@ -135,6 +135,9 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 ``machete.gitlab.forceDescriptionFromCommitMessage`` (``create-mr`` only):
   .. include:: git-config-keys/gitlab_forceDescriptionFromCommitMessage.rst
 
+``machete.gitlab.mrDescriptionIntroStyle`` (``create-mr``, ``restack-mr`` and ``retarget-mr``):
+  .. include:: git-config-keys/gitlab_mrDescriptionIntroStyle.rst
+
 **Environment variables (all subcommands):**
 
 ``GITLAB_TOKEN``
diff --git a/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst b/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst
new file mode 100644
index 0000000..2948906
--- /dev/null
+++ b/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst
@@ -0,0 +1,4 @@
+Select the style of the intro prepended to PR description:
+  * ``full``    --- include both a chain of upstream PRs (typically leading to ``main``, ``master``, ``develop`` etc.) and a tree of downstream PRs
+  * ``up-only`` --- default, include only a chain of upstream PRs
+  * ``none``    --- prepend no intro to the PR description at all
diff --git a/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst b/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst
new file mode 100644
index 0000000..8465072
--- /dev/null
+++ b/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst
@@ -0,0 +1,4 @@
+Select the style of the intro prepended to MR description:
+  * ``full``    --- include both a chain of upstream MRs (typically leading to ``main``, ``master``, ``develop`` etc.) and a tree of downstream MRs
+  * ``up-only`` --- default, include only a chain of upstream MRs
+  * ``none``    --- prepend no intro to the MR description at all
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 89c638d..c9c9832 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.28.0'
+__version__ = '3.29.0'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index afda4d3..538ea84 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -11,11 +11,11 @@ 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
 
-from .client import MacheteClient
+from .client import (MacheteClient, SquashMergeDetection, TraverseReturnTo,
+                     TraverseStartFrom)
 from .exceptions import (ExitCode, InteractionStopped, MacheteException,
                          UnderlyingGitException, UnexpectedMacheteException)
 from .generated_docs import long_docs, short_docs
@@ -901,8 +901,10 @@ def launch(orig_args: List[str]) -> None:
             if cli_opts.opt_start_from not in {"here", "root", "first-root"}:
                 raise MacheteException(f"Invalid value for `--start-from` flag: `{cli_opts.opt_start_from}`. "
                                        "Valid values are here, root, first-root")
+            opt_return_to = TraverseReturnTo.from_string(cli_opts.opt_return_to, "`--return-to` flag")
             opt_squash_merge_detection = SquashMergeDetection.from_string(
                 cli_opts.opt_squash_merge_detection_string, cli_opts.opt_squash_merge_detection_origin)
+            opt_start_from = TraverseStartFrom.from_string(cli_opts.opt_start_from, "`--start-from` flag")
 
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
@@ -914,9 +916,9 @@ def launch(orig_args: List[str]) -> None:
                 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_return_to=opt_return_to,
                 opt_squash_merge_detection=opt_squash_merge_detection,
-                opt_start_from=cli_opts.opt_start_from,
+                opt_start_from=opt_start_from,
                 opt_yes=cli_opts.opt_yes)
         elif cmd == "update":
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
diff --git a/git_machete/client.py b/git_machete/client.py
index 7f1e27f..4299f49 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -8,7 +8,8 @@ import sys
 import textwrap
 from collections import OrderedDict
 from enum import Enum, auto
-from typing import Callable, Dict, Iterator, List, Optional, Tuple
+from typing import (Callable, Dict, Iterator, List, Optional, Set, Tuple, Type,
+                    TypeVar)
 
 from . import git_config_keys, utils
 from .annotation import Annotation
@@ -17,21 +18,86 @@ from .code_hosting import (CodeHostingClient, CodeHostingSpec,
                            OrganizationAndRepositoryAndRemote, PullRequest,
                            is_matching_remote_url)
 from .constants import (DISCOVER_DEFAULT_FRESH_BRANCH_COUNT,
-                        INITIAL_COMMIT_COUNT_FOR_LOG, PICK_FIRST_ROOT,
-                        PICK_LAST_ROOT, TOTAL_COMMIT_COUNT_FOR_LOG,
-                        GitFormatPatterns, SquashMergeDetection,
-                        SyncToRemoteStatuses)
+                        INITIAL_COMMIT_COUNT_FOR_LOG,
+                        TOTAL_COMMIT_COUNT_FOR_LOG)
 from .exceptions import (InteractionStopped, MacheteException,
                          UnexpectedMacheteException)
 from .git_operations import (HEAD, AnyBranchName, AnyRevision, BranchPair,
                              ForkPointOverrideData, FullCommitHash, GitContext,
-                             GitLogEntry, LocalBranchShortName,
-                             RemoteBranchShortName)
+                             GitFormatPatterns, GitLogEntry,
+                             LocalBranchShortName, RemoteBranchShortName,
+                             SyncToRemoteStatus)
 from .utils import (AnsiEscapeCodes, PopenResult, bold, colored, debug, dim,
                     excluding, flat_map, fmt, get_pretty_choices, get_second,
                     tupled, underline, warn)
 
 
+class SyncToParentStatus(Enum):
+    InSync = auto()
+    MergedToParent = auto()
+    InSyncButForkPointOff = auto()
+    OutOfSync = auto()
+
+
+sync_to_parent_status_to_edge_color_map: Dict[SyncToParentStatus, str] = {
+    SyncToParentStatus.MergedToParent: AnsiEscapeCodes.DIM,
+    SyncToParentStatus.InSync: AnsiEscapeCodes.GREEN,
+    SyncToParentStatus.InSyncButForkPointOff: AnsiEscapeCodes.YELLOW,
+    SyncToParentStatus.OutOfSync: AnsiEscapeCodes.RED
+}
+
+sync_to_parent_status_to_junction_ascii_only_map: Dict[SyncToParentStatus, str] = {
+    SyncToParentStatus.MergedToParent: "m-",
+    SyncToParentStatus.InSync: "o-",
+    SyncToParentStatus.InSyncButForkPointOff: "?-",
+    SyncToParentStatus.OutOfSync: "x-"
+}
+
+
+E = TypeVar('E', bound='Enum')
+
+
+class ParsableEnum(Enum):
+    @classmethod
+    def from_string(cls: Type[E], value: str, from_where: Optional[str]) -> E:
+        try:
+            return cls[value.upper().replace("-", "_")]
+        except KeyError:
+            valid_values = ', '.join('`' + e.name.lower().replace("_", "-") + '`' for e in cls)
+            prefix = f"Invalid value for {from_where}" if from_where else "Invalid value"
+            printed_value = value or '<empty>'
+            raise MacheteException(f"{prefix}: `{printed_value}`. Valid values are {valid_values}")
+
+
+class PickRoot(Enum):
+    FIRST = auto()
+    LAST = auto()
+
+
+class PRDescriptionIntroStyle(ParsableEnum):
+    FULL = auto()
+    UP_ONLY = auto()
+    NONE = auto()
+
+
+class SquashMergeDetection(ParsableEnum):
+    NONE = auto()
+    SIMPLE = auto()
+    EXACT = auto()
+
+
+class TraverseReturnTo(ParsableEnum):
+    HERE = auto()
+    NEAREST_REMAINING = auto()
+    STAY = auto()  # noqa: F841
+
+
+class TraverseStartFrom(ParsableEnum):
+    HERE = auto()
+    ROOT = auto()
+    FIRST_ROOT = auto()
+
+
 class MacheteClient:
 
     def __init__(self, git: GitContext) -> None:
@@ -695,9 +761,9 @@ class MacheteClient:
             opt_no_interactive_rebase: bool,
             opt_push_tracked: bool,
             opt_push_untracked: bool,
-            opt_return_to: str,
+            opt_return_to: TraverseReturnTo,
             opt_squash_merge_detection: SquashMergeDetection,
-            opt_start_from: str,
+            opt_start_from: TraverseStartFrom,
             opt_yes: bool
     ) -> None:
         self.expect_at_least_one_managed_branch()
@@ -714,20 +780,20 @@ class MacheteClient:
 
         initial_branch = nearest_remaining_branch = self.__git.get_current_branch()
 
-        if opt_start_from == "root":
-            dest = self.root_branch(self.__git.get_current_branch(), if_unmanaged=PICK_FIRST_ROOT)
+        if opt_start_from == TraverseStartFrom.ROOT:
+            dest = self.root_branch(self.__git.get_current_branch(), if_unmanaged=PickRoot.FIRST)
             self.__print_new_line(False)
             print(f"Checking out the root branch ({bold(dest)})")
             self.__git.checkout(dest)
             current_branch = dest
-        elif opt_start_from == "first-root":
+        elif opt_start_from == TraverseStartFrom.FIRST_ROOT:
             # Note that we already ensured that there is at least one managed branch.
             dest = self.managed_branches[0]
             self.__print_new_line(False)
             print(f"Checking out the first root branch ({bold(dest)})")
             self.__git.checkout(dest)
             current_branch = dest
-        else:  # cli_opts.opt_start_from == "here"
+        else:  # cli_opts.opt_start_from == TraverseStartFrom.HERE
             current_branch = self.__git.get_current_branch()
             self.expect_in_managed_branches(current_branch)
 
@@ -741,13 +807,13 @@ class MacheteClient:
                 needs_slide_out = self.annotations[branch].qualifiers.slide_out
             s, remote = self.__git.get_combined_remote_sync_status(branch)
             if s in (
-                    SyncToRemoteStatuses.BEHIND_REMOTE,
-                    SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE):
+                    SyncToRemoteStatus.BEHIND_REMOTE,
+                    SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE):
                 needs_remote_sync = True
             elif s in (
-                    SyncToRemoteStatuses.UNTRACKED,
-                    SyncToRemoteStatuses.AHEAD_OF_REMOTE,
-                    SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE):
+                    SyncToRemoteStatus.UNTRACKED,
+                    SyncToRemoteStatus.AHEAD_OF_REMOTE,
+                    SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE):
                 needs_remote_sync = True
                 if branch in self.annotations:
                     needs_remote_sync = self.annotations[branch].qualifiers.push
@@ -763,7 +829,7 @@ class MacheteClient:
                 # branch qualifies for slide out;
                 # neither rebase nor merge will be suggested in such case anyway.
                 needs_parent_sync: bool = False
-            elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+            elif s == SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
                 # Avoid unnecessary fork point check if we already know that the
                 # branch qualifies for resetting to remote counterpart;
                 # neither rebase nor merge will be suggested in such case anyway.
@@ -878,8 +944,8 @@ class MacheteClient:
 
                     s, remote = self.__git.get_combined_remote_sync_status(branch)
                     if s in (
-                            SyncToRemoteStatuses.BEHIND_REMOTE,
-                            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE):
+                            SyncToRemoteStatus.BEHIND_REMOTE,
+                            SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE):
                         # This case is extremely unlikely in practice.
                         #
                         # For a freshly rebased/merged branch to be behind its remote, would require a specially crafted scenario
@@ -891,9 +957,9 @@ class MacheteClient:
                         # is still considered "logically newer" than the "physically newer" rebased/merge commit).
                         needs_remote_sync = True
                     elif s in (
-                            SyncToRemoteStatuses.UNTRACKED,
-                            SyncToRemoteStatuses.AHEAD_OF_REMOTE,
-                            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE):
+                            SyncToRemoteStatus.UNTRACKED,
+                            SyncToRemoteStatus.AHEAD_OF_REMOTE,
+                            SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE):
                         needs_remote_sync = True
                         if branch in self.annotations:
                             needs_remote_sync = self.annotations[branch].qualifiers.push
@@ -906,10 +972,10 @@ class MacheteClient:
             if needs_remote_sync:
                 any_action_suggested = True
                 try:
-                    if s == SyncToRemoteStatuses.BEHIND_REMOTE:
+                    if s == SyncToRemoteStatus.BEHIND_REMOTE:
                         assert remote is not None
                         self.__handle_behind_state(current_branch, remote, opt_yes=opt_yes)
-                    elif s == SyncToRemoteStatuses.AHEAD_OF_REMOTE:
+                    elif s == SyncToRemoteStatus.AHEAD_OF_REMOTE:
                         assert remote is not None
                         self.__handle_ahead_state(
                             current_branch=current_branch,
@@ -917,16 +983,16 @@ class MacheteClient:
                             is_called_from_traverse=True,
                             opt_push_tracked=opt_push_tracked,
                             opt_yes=opt_yes)
-                    elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+                    elif s == SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
                         self.__handle_diverged_and_older_state(current_branch, opt_yes=opt_yes)
-                    elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
+                    elif s == SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
                         assert remote is not None
                         self.__handle_diverged_and_newer_state(
                             current_branch=current_branch,
                             remote=remote,
                             opt_push_tracked=opt_push_tracked,
                             opt_yes=opt_yes)
-                    elif s == SyncToRemoteStatuses.UNTRACKED:
+                    elif s == SyncToRemoteStatus.UNTRACKED:
                         self.__handle_untracked_state(
                             branch=current_branch,
                             is_called_from_traverse=True,
@@ -939,11 +1005,11 @@ class MacheteClient:
                 except InteractionStopped:
                     return
 
-        if opt_return_to == "here":
+        if opt_return_to == TraverseReturnTo.HERE:
             self.__git.checkout(initial_branch)
-        elif opt_return_to == "nearest-remaining":
+        elif opt_return_to == TraverseReturnTo.NEAREST_REMAINING:
             self.__git.checkout(nearest_remaining_branch)
-        # otherwise opt_return_to == "stay", so no action is needed
+        # otherwise opt_return_to == TraverseReturnTo.STAY, so no action is needed
 
         self.__print_new_line(False)
         self.status(
@@ -961,10 +1027,10 @@ class MacheteClient:
             print(fmt("Tip: `traverse` by default starts from the current branch, "
                       "use flags (`--start-from=`, `--whole` or `-w`, `-W`) to change this behavior.\n"
                       "Further info under `git machete traverse --help`."))
-        if opt_return_to == "here" or (
-                opt_return_to == "nearest-remaining" and nearest_remaining_branch == initial_branch):
+        if opt_return_to == TraverseReturnTo.HERE or (
+                opt_return_to == TraverseReturnTo.NEAREST_REMAINING and nearest_remaining_branch == initial_branch):
             print(f"Returned to the initial branch {bold(initial_branch)}")
-        elif opt_return_to == "nearest-remaining" and nearest_remaining_branch != initial_branch:
+        elif opt_return_to == TraverseReturnTo.NEAREST_REMAINING and nearest_remaining_branch != initial_branch:
             print(
                 f"The initial branch {bold(initial_branch)} has been slid out. "
                 f"Returned to nearest remaining managed branch {bold(nearest_remaining_branch)}")
@@ -1123,18 +1189,18 @@ class MacheteClient:
 
             s, remote = self.__git.get_combined_remote_sync_status(branch)
             sync_status = {
-                SyncToRemoteStatuses.NO_REMOTES: "",
-                SyncToRemoteStatuses.UNTRACKED: colored(" (untracked)", AnsiEscapeCodes.ORANGE),
-                SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE: "",
-                SyncToRemoteStatuses.BEHIND_REMOTE:
+                SyncToRemoteStatus.NO_REMOTES: "",
+                SyncToRemoteStatus.UNTRACKED: colored(" (untracked)", AnsiEscapeCodes.ORANGE),
+                SyncToRemoteStatus.IN_SYNC_WITH_REMOTE: "",
+                SyncToRemoteStatus.BEHIND_REMOTE:
                     colored(f" (behind {bold(remote)})", AnsiEscapeCodes.RED),  # type: ignore [arg-type]
-                SyncToRemoteStatuses.AHEAD_OF_REMOTE:
+                SyncToRemoteStatus.AHEAD_OF_REMOTE:
                     colored(f" (ahead of {bold(remote)})", AnsiEscapeCodes.RED),  # type: ignore [arg-type]
-                SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+                SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
                     colored(f" (diverged from & older than {bold(remote)})", AnsiEscapeCodes.RED),  # type: ignore [arg-type]
-                SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
+                SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
                     colored(f" (diverged from {bold(remote)})", AnsiEscapeCodes.RED)  # type: ignore [arg-type]
-            }[SyncToRemoteStatuses(s)]
+            }[SyncToRemoteStatus(s)]
 
             hook_output = ""
             if hook_executable:
@@ -1445,12 +1511,12 @@ class MacheteClient:
             return dbs
 
     def first_branch(self, branch: LocalBranchShortName) -> LocalBranchShortName:
-        root = self.root_branch(branch, if_unmanaged=PICK_FIRST_ROOT)
+        root = self.root_branch(branch, if_unmanaged=PickRoot.FIRST)
         root_dbs = self.__down_branches.get(root)
         return root_dbs[0] if root_dbs else root
 
     def last_branch(self, branch: LocalBranchShortName) -> LocalBranchShortName:
-        destination = self.root_branch(branch, if_unmanaged=PICK_LAST_ROOT)
+        destination = self.root_branch(branch, if_unmanaged=PickRoot.LAST)
         while self.__down_branches.get(destination):
             destination = self.__down_branches[destination][-1]
         return destination
@@ -1469,15 +1535,15 @@ class MacheteClient:
             raise MacheteException(f"Branch {bold(branch)} has no predecessor")
         return self.managed_branches[index]
 
-    def root_branch(self, branch: LocalBranchShortName, if_unmanaged: int) -> LocalBranchShortName:
+    def root_branch(self, branch: LocalBranchShortName, if_unmanaged: PickRoot) -> LocalBranchShortName:
         if branch not in self.managed_branches:
             if self.__roots:
-                if if_unmanaged == PICK_FIRST_ROOT:
+                if if_unmanaged == PickRoot.FIRST:
                     warn(
                         f"{bold(branch)} is not a managed branch, assuming "
                         f"{self.__roots[0]} (the first root) instead as root")
                     return self.__roots[0]
-                else:  # if_unmanaged == PICK_LAST_ROOT
+                else:  # if_unmanaged == PickRoot.LAST
                     warn(
                         f"{bold(branch)} is not a managed branch, assuming "
                         f"{self.__roots[-1]} (the last root) instead as root")
@@ -1712,7 +1778,7 @@ class MacheteClient:
         elif param in ("p", "prev"):
             return [self.prev_branch(branch)]
         elif param in ("r", "root"):
-            return [self.root_branch(branch, if_unmanaged=PICK_FIRST_ROOT)]
+            return [self.root_branch(branch, if_unmanaged=PickRoot.FIRST)]
         elif param in ("u", "up"):
             return [self.up(branch, prompt_if_inferred_msg=None, prompt_if_inferred_yes_opt_msg=None)]
         else:  # an unknown direction is handled by argparse
@@ -1946,69 +2012,69 @@ class MacheteClient:
                     raise InteractionStopped
                 return
 
-        relation: int = self.__git.get_relation_to_remote_counterpart(branch, remote_branch)
+        relation = self.__git.get_relation_to_remote_counterpart(branch, remote_branch)
 
         message: str = {
-            SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE:
+            SyncToRemoteStatus.IN_SYNC_WITH_REMOTE:
                 f"Branch {bold(branch)} is untracked, but its remote counterpart candidate {bold(remote_branch)} "
                 f"already exists and both branches point to the same commit.",
-            SyncToRemoteStatuses.BEHIND_REMOTE:
+            SyncToRemoteStatus.BEHIND_REMOTE:
                 f"Branch {bold(branch)} is untracked, but its remote counterpart candidate {bold(remote_branch)} "
                 f"already exists and is ahead of {bold(branch)}.",
-            SyncToRemoteStatuses.AHEAD_OF_REMOTE:
+            SyncToRemoteStatus.AHEAD_OF_REMOTE:
                 f"Branch {bold(branch)} is untracked, but its remote counterpart candidate {bold(remote_branch)} "
                 f"already exists and is behind {bold(branch)}.",
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+            SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
                 f"Branch {bold(branch)} is untracked, it diverged from its remote counterpart candidate {bold(remote_branch)}, "
                 f"and has {bold('older')} commits than {bold(remote_branch)}.",
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
+            SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
                 f"Branch {bold(branch)} is untracked, it diverged from its remote counterpart candidate {bold(remote_branch)}, "
                 f"and has {bold('newer')} commits than {bold(remote_branch)}."
-        }[SyncToRemoteStatuses(relation)]
+        }[SyncToRemoteStatus(relation)]
 
         ask_message, ask_opt_yes_message = {
-            SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE: (
+            SyncToRemoteStatus.IN_SYNC_WITH_REMOTE: (
                 f"Set the remote of {bold(branch)} to {bold(new_remote)} without pushing or pulling?" +
                 get_pretty_choices('y', 'N', 'q', 'yq', other_remote_choice),
                 f"Setting the remote of {bold(branch)} to {bold(new_remote)}..."
             ),
-            SyncToRemoteStatuses.BEHIND_REMOTE: (
+            SyncToRemoteStatus.BEHIND_REMOTE: (
                 f"Pull {bold(branch)} (fast-forward only) from {bold(new_remote)}?" + get_pretty_choices('y', 'N', 'q', 'yq',
                                                                                                          other_remote_choice),
                 f"Pulling {bold(branch)} (fast-forward only) from {bold(new_remote)}..."
             ),
-            SyncToRemoteStatuses.AHEAD_OF_REMOTE: (
+            SyncToRemoteStatus.AHEAD_OF_REMOTE: (
                 f"Push branch {bold(branch)} to {bold(new_remote)}?" + get_pretty_choices('y', 'N', 'q', 'yq', other_remote_choice),
                 f"Pushing branch {bold(branch)} to {bold(new_remote)}..."
             ),
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: (
+            SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: (
                 f"Reset branch {bold(branch)} to the commit pointed by {bold(remote_branch)}?" + get_pretty_choices('y', 'N', 'q', 'yq',
                                                                                                                     other_remote_choice),
                 f"Resetting branch {bold(branch)} to the commit pointed by {bold(remote_branch)}..."
             ),
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: (
+            SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: (
                 f"Push branch {bold(branch)} with force-with-lease to {bold(new_remote)}?" + get_pretty_choices('y', 'N', 'q', 'yq',
                                                                                                                 other_remote_choice),
                 f"Pushing branch {bold(branch)} with force-with-lease to {bold(new_remote)}..."
             )
-        }[SyncToRemoteStatuses(relation)]
+        }[SyncToRemoteStatus(relation)]
 
         override_answer: Optional[str] = {
-            SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE: None,
-            SyncToRemoteStatuses.BEHIND_REMOTE: None,
-            SyncToRemoteStatuses.AHEAD_OF_REMOTE: None if opt_push_tracked else "N",
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: None,
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: None if opt_push_tracked else "N",
-        }[SyncToRemoteStatuses(relation)]
+            SyncToRemoteStatus.IN_SYNC_WITH_REMOTE: None,
+            SyncToRemoteStatus.BEHIND_REMOTE: None,
+            SyncToRemoteStatus.AHEAD_OF_REMOTE: None if opt_push_tracked else "N",
+            SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: None,
+            SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: None if opt_push_tracked else "N",
+        }[SyncToRemoteStatus(relation)]
 
         yes_action: Callable[[], None] = {
-            SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE: lambda: self.__git.set_upstream_to(remote_branch),
-            SyncToRemoteStatuses.BEHIND_REMOTE: lambda: self.__git.pull_ff_only(new_remote, remote_branch),
-            SyncToRemoteStatuses.AHEAD_OF_REMOTE: lambda: self.__git.push(new_remote, branch),
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: lambda: self.__git.reset_keep(remote_branch),
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: lambda: self.__git.push(
+            SyncToRemoteStatus.IN_SYNC_WITH_REMOTE: lambda: self.__git.set_upstream_to(remote_branch),
+            SyncToRemoteStatus.BEHIND_REMOTE: lambda: self.__git.pull_ff_only(new_remote, remote_branch),
+            SyncToRemoteStatus.AHEAD_OF_REMOTE: lambda: self.__git.push(new_remote, branch),
+            SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE: lambda: self.__git.reset_keep(remote_branch),
+            SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE: lambda: self.__git.push(
                 new_remote, branch, force_with_lease=True)
-        }[SyncToRemoteStatuses(relation)]
+        }[SyncToRemoteStatus(relation)]
 
         print(message)
         ans = self.ask_if(ask_message, ask_opt_yes_message, override_answer=override_answer, opt_yes=opt_yes)
@@ -2161,7 +2227,7 @@ class MacheteClient:
                 warn(f'{pr.display_text()} is already closed.')
             debug(f'found {pr}')
 
-            pr_path: List[PullRequest] = self.__get_path_from_pr_chain(spec, pr, all_open_prs)
+            pr_path: List[PullRequest] = self.__get_upwards_path_including_pr(spec, pr, all_open_prs)
             prs_to_annotate.update(pr_path)
             reversed_pr_path: List[PullRequest] = pr_path[::-1]  # need to add from root downwards
             if reversed_pr_path[0].base not in self.managed_branches:
@@ -2192,7 +2258,24 @@ class MacheteClient:
             self.__git.checkout(LocalBranchShortName.of(applicable_prs[0].head))
 
     @staticmethod
-    def __get_path_from_pr_chain(spec: CodeHostingSpec, original_pr: PullRequest, all_open_prs: List[PullRequest]) -> List[PullRequest]:
+    def __get_downwards_tree_excluding_pr(original_pr: PullRequest,
+                                          all_open_prs: List[PullRequest]) -> List[Tuple[PullRequest, int]]:
+        """Returns pairs of (PR, depth below the given PR)"""
+
+        visited_head_branches: Set[str] = set([])
+
+        def reverse_pr_dfs(pr: PullRequest, depth: int) -> Iterator[Tuple[PullRequest, int]]:
+            visited_head_branches.add(pr.head)
+            down_prs = filter(lambda x: x.base == pr.head, all_open_prs)
+            for down_pr in sorted(down_prs, key=lambda x: x.number):
+                if down_pr.head not in visited_head_branches:
+                    yield (down_pr, depth + 1)
+                    yield from reverse_pr_dfs(down_pr, depth + 1)
+        return list(reverse_pr_dfs(original_pr, 0))
+
+    @staticmethod
+    def __get_upwards_path_including_pr(spec: CodeHostingSpec, original_pr: PullRequest,
+                                        all_open_prs: List[PullRequest]) -> List[PullRequest]:
         visited_head_branches: List[str] = [original_pr.head]
         path: List[PullRequest] = [original_pr]
         pr_base: Optional[str] = original_pr.base
@@ -2298,9 +2381,9 @@ class MacheteClient:
         current_branch = self.__git.get_current_branch()
         s, remote = self.__git.get_combined_remote_sync_status(current_branch)
         statuses_to_push = (
-            SyncToRemoteStatuses.UNTRACKED,
-            SyncToRemoteStatuses.AHEAD_OF_REMOTE,
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
+            SyncToRemoteStatus.UNTRACKED,
+            SyncToRemoteStatus.AHEAD_OF_REMOTE,
+            SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
         if s in statuses_to_push:
             if current_branch in self.annotations and not self.annotations[current_branch].qualifiers.push:
                 subcommand = "retarget-" + spec.pr_short_name.lower()
@@ -2315,7 +2398,7 @@ class MacheteClient:
             # Note that retarget should happen BEFORE push, see issue #1222
             self.retarget_pr(spec, head, ignore_if_missing=False)
 
-            if s == SyncToRemoteStatuses.AHEAD_OF_REMOTE:
+            if s == SyncToRemoteStatus.AHEAD_OF_REMOTE:
                 assert remote is not None
                 self.__handle_ahead_state(
                     current_branch=current_branch,
@@ -2323,7 +2406,7 @@ class MacheteClient:
                     is_called_from_traverse=False,
                     opt_push_tracked=True,
                     opt_yes=True)
-            elif s == SyncToRemoteStatuses.UNTRACKED:
+            elif s == SyncToRemoteStatus.UNTRACKED:
                 self.__handle_untracked_state(
                     branch=current_branch,
                     is_called_from_traverse=False,
@@ -2331,7 +2414,7 @@ class MacheteClient:
                     opt_push_tracked=True,
                     opt_push_untracked=True,
                     opt_yes=True)
-            elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
+            elif s == SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
                 assert remote is not None
                 self.__handle_diverged_and_newer_state(
                     current_branch=current_branch,
@@ -2355,12 +2438,12 @@ class MacheteClient:
                 print(f'{pr.display_text()} has been marked as ready for review again')
 
         else:
-            if s == SyncToRemoteStatuses.BEHIND_REMOTE:
+            if s == SyncToRemoteStatus.BEHIND_REMOTE:
                 warn(f"Branch {bold(current_branch)} is behind its remote counterpart. Consider using `git pull`.\n")
-            elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+            elif s == SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
                 warn(f"Branch {bold(current_branch)} is diverged from and older than its remote counterpart. "
                      "Consider using `git reset --keep`.\n")
-            elif s == SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE:
+            elif s == SyncToRemoteStatus.IN_SYNC_WITH_REMOTE:
                 pass
             else:  # case handled elsewhere
                 raise UnexpectedMacheteException(f"Could not retarget {spec.pr_full_name}: invalid sync-to-remote status `{s}`.")
@@ -2373,7 +2456,8 @@ class MacheteClient:
             return list(itertools.dropwhile(lambda line: line.strip() == '', strs))
 
         lines = skip_leading_empty(old_description.splitlines()) if old_description else []
-        text_to_prepend = self.__generate_text_to_prepend_to_pr_description(code_hosting_client, pr)
+        style = self.__get_pr_description_into_style_from_config(code_hosting_client._spec)
+        text_to_prepend = self.__generate_pr_description_intro(code_hosting_client, pr, style)
         lines_to_prepend = text_to_prepend.splitlines() if text_to_prepend else []
         if self.START_GIT_MACHETE_GENERATED_COMMENT in lines and self.END_GIT_MACHETE_GENERATED_COMMENT in lines:
             start_index = lines.index(self.START_GIT_MACHETE_GENERATED_COMMENT)
@@ -2533,31 +2617,68 @@ class MacheteClient:
     START_GIT_MACHETE_GENERATED_COMMENT = '<!-- start git-machete generated -->'
     END_GIT_MACHETE_GENERATED_COMMENT = '<!-- end git-machete generated -->'
 
-    def __generate_text_to_prepend_to_pr_description(self, code_hosting_client: CodeHostingClient, pr: PullRequest) -> str:
+    def __get_pr_description_into_style_from_config(self, spec: CodeHostingSpec) -> PRDescriptionIntroStyle:
+        config_key = spec.git_config_keys.pr_description_intro_style
+        return PRDescriptionIntroStyle.from_string(
+            value=self.__git.get_config_attr(key=config_key, default_value="up-only"),
+            from_where=f"`{config_key}` git config key"
+        )
 
-        prs_for_base_branch = code_hosting_client.get_open_pull_requests_by_head(LocalBranchShortName(pr.base))
-        if len(prs_for_base_branch) < 1:
+    def __generate_pr_description_intro(self, code_hosting_client: CodeHostingClient,
+                                        pr: PullRequest, style: PRDescriptionIntroStyle) -> str:
+        if style == PRDescriptionIntroStyle.NONE:
             return ''
+
         # For determining the PR chain, we need to fetch all PRs from the repo.
         # We could just fetch them straight away... but this list can be quite long for commercial monorepos,
-        # esp. given that GitHub and GitLab limit the single page to 100 PRs (so multiple HTTP requests would be needed).
-        # As a slight optimization, let's fetch the full PR list only if the current PR has a base PR at all.
-        config = code_hosting_client._spec
-        display_name = config.display_name
-        pr_short_name = config.pr_short_name
-        print(f'Checking for open {display_name} {pr_short_name}s (to determine {pr_short_name} chain)... ', end='', flush=True)
+        # esp. given that GitHub and GitLab limit the single page to 100 PRs/MRs (so multiple HTTP requests may be needed).
+        # As a slight optimization, in the default UP_ONLY style,
+        # let's fetch the full PR list only if the current PR has a base PR at all.
+        prs_for_base_branch = code_hosting_client.get_open_pull_requests_by_head(LocalBranchShortName(pr.base))
+        if style == PRDescriptionIntroStyle.UP_ONLY and len(prs_for_base_branch) == 0:
+            return ''
+        spec = code_hosting_client._spec
+        display_name = spec.display_name
+        pr_short_name = spec.pr_short_name
+        determine_what = 'chain' if style == PRDescriptionIntroStyle.UP_ONLY else 'tree'
+        print(f'Checking for open {display_name} {pr_short_name}s (to determine {pr_short_name} {determine_what})... ', end='', flush=True)
         all_open_prs: List[PullRequest] = code_hosting_client.get_open_pull_requests()
         print(fmt('<green><b>OK</b></green>'))
-        pr_path = self.__get_path_from_pr_chain(config, pr, all_open_prs)
+        pr_up_path = reversed(self.__get_upwards_path_including_pr(spec, pr, all_open_prs))
+        if style == PRDescriptionIntroStyle.FULL:
+            pr_down_tree = self.__get_downwards_tree_excluding_pr(pr, all_open_prs)
+        else:
+            pr_down_tree = []
 
         prepend = f'{self.START_GIT_MACHETE_GENERATED_COMMENT}\n\n'
-        prepend += f'# Based on {prs_for_base_branch[0].display_text(fmt=False)}\n\n'
+        # In FULL mode, we're likely to generate the intro even when there are NO upstream PRs above
+        if len(prs_for_base_branch) >= 1:
+            prepend += f'# Based on {prs_for_base_branch[0].display_text(fmt=False)}\n\n'
+
+        if pr_down_tree:
+            prepend += f'## Chain of upstream {pr_short_name}s & tree of downstream {pr_short_name}s'
+        else:
+            prepend += f'## Chain of upstream {pr_short_name}s'
         current_date = utils.get_current_date()
-        prepend += f'## Full chain of {pr_short_name}s as of {current_date}\n\n'
-        for ancestor_pr in pr_path:
-            prepend += f'* {ancestor_pr.display_text(fmt=False)}:\n'
-            prepend += f'  `{ancestor_pr.head}` ➔ `{ancestor_pr.base}`\n'
-        prepend += '\n'
+        prepend += f' as of {current_date}\n\n'
+
+        def pr_entry(_pr: PullRequest, _depth: int) -> str:
+            result = '  ' * _depth
+            display_text = _pr.display_text(fmt=False)
+            if _pr.number == pr.number:
+                result += f'* **{display_text} (THIS ONE)**:\n'
+            else:
+                result += f'* {display_text}:\n'
+            result += '  ' * _depth
+            result += f'  `{_pr.base}` ← `{_pr.head}`\n\n'
+            return result
+
+        base_depth = 0
+        for up_pr in pr_up_path:
+            prepend += pr_entry(up_pr, base_depth)
+            base_depth += 1
+        for (down_pr, depth) in pr_down_tree:
+            prepend += pr_entry(down_pr, base_depth + depth)
         prepend += f'{self.END_GIT_MACHETE_GENERATED_COMMENT}\n'
         return prepend
 
@@ -2662,12 +2783,14 @@ class MacheteClient:
             title=title, description=description, draft=opt_draft)
         print(fmt(f'{ok_str}, see `{pr.html_url}`'))
 
+        style = self.__get_pr_description_into_style_from_config(spec)
         # If base branch has NOT originally been found on the remote,
-        # we can be sure that a longer chain of PRs above the newly-created PR does NOT exist
-        if base_branch_found_on_remote:
+        # we can be sure that a longer chain of PRs above the newly-created PR does NOT exist.
+        # So in the default UP_ONLY mode, we can skip generating the intro completely.
+        if base_branch_found_on_remote or style == PRDescriptionIntroStyle.FULL:
             # As the description may include the reference to this PR itself (in case of a chain of >=2 PRs),
             # let's update the PR description after it's already created (so that we know the current PR's number).
-            text_to_prepend = self.__generate_text_to_prepend_to_pr_description(code_hosting_client, pr)
+            text_to_prepend = self.__generate_pr_description_intro(code_hosting_client, pr, style)
             if text_to_prepend:
                 if description:
                     text_to_prepend += '\n'
@@ -2869,12 +2992,12 @@ class MacheteClient:
 
         s, remote = self.__git.get_combined_remote_sync_status(current_branch)
         statuses_to_push = (
-            SyncToRemoteStatuses.UNTRACKED,
-            SyncToRemoteStatuses.AHEAD_OF_REMOTE,
-            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
+            SyncToRemoteStatus.UNTRACKED,
+            SyncToRemoteStatus.AHEAD_OF_REMOTE,
+            SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
         if s in statuses_to_push:
             if current_branch not in self.annotations or self.annotations[current_branch].qualifiers.push:
-                if s == SyncToRemoteStatuses.AHEAD_OF_REMOTE:
+                if s == SyncToRemoteStatus.AHEAD_OF_REMOTE:
                     assert remote is not None
                     self.__handle_ahead_state(
                         current_branch=current_branch,
@@ -2882,7 +3005,7 @@ class MacheteClient:
                         is_called_from_traverse=False,
                         opt_push_tracked=True,
                         opt_yes=opt_yes)
-                elif s == SyncToRemoteStatuses.UNTRACKED:
+                elif s == SyncToRemoteStatus.UNTRACKED:
                     self.__handle_untracked_state(
                         branch=current_branch,
                         is_called_from_traverse=False,
@@ -2890,7 +3013,7 @@ class MacheteClient:
                         opt_push_tracked=True,
                         opt_push_untracked=True,
                         opt_yes=opt_yes)
-                elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
+                elif s == SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
                     assert remote is not None
                     self.__handle_diverged_and_newer_state(
                         current_branch=current_branch,
@@ -2910,18 +3033,18 @@ class MacheteClient:
                 self.__print_new_line(False)
 
         else:
-            if s == SyncToRemoteStatuses.BEHIND_REMOTE:
+            if s == SyncToRemoteStatus.BEHIND_REMOTE:
                 warn(f"Branch {bold(current_branch)} is behind its remote counterpart. Consider using `git pull`.")
                 self.__print_new_line(False)
                 ans = self.ask_if(f"Proceed with creating {spec.pr_full_name}?" + get_pretty_choices('y', 'Q'),
                                   f"Proceeding with {spec.pr_full_name} creation...", opt_yes=opt_yes)
-            elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
+            elif s == SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE:
                 warn(f"Branch {bold(current_branch)} is diverged from and older than its remote counterpart. "
                      "Consider using `git reset --keep`.")
                 self.__print_new_line(False)
                 ans = self.ask_if(f"Proceed with creating {spec.pr_full_name}?" + get_pretty_choices('y', 'Q'),
                                   f"Proceeding with {spec.pr_full_name} creation...", opt_yes=opt_yes)
-            elif s == SyncToRemoteStatuses.NO_REMOTES:
+            elif s == SyncToRemoteStatus.NO_REMOTES:
                 raise MacheteException(
                     f"Could not create {spec.pr_full_name} - there are no remote repositories!")
             else:
@@ -2936,7 +3059,7 @@ class MacheteClient:
         branches_to_delete: List[LocalBranchShortName] = []
         for branch in self.managed_branches.copy():
             status, _ = self.__git.get_combined_remote_sync_status(branch)
-            if status == SyncToRemoteStatuses.UNTRACKED and not self.__down_branches.get(branch):
+            if status == SyncToRemoteStatus.UNTRACKED and not self.__down_branches.get(branch):
                 branches_to_delete.append(branch)
 
         self.__remove_branches_from_layout(branches_to_delete)
@@ -2977,25 +3100,3 @@ class MacheteClient:
     def should_perform_interactive_slide_out(cmd: str) -> bool:
         interactive_slide_out_safe_commands = {'traverse', 'status'}
         return MacheteClient.is_stdout_a_tty() and cmd in interactive_slide_out_safe_commands
-
-
-class SyncToParentStatus(Enum):
-    InSync = auto()
-    MergedToParent = auto()
-    InSyncButForkPointOff = auto()
-    OutOfSync = auto()
-
-
-sync_to_parent_status_to_edge_color_map: Dict[SyncToParentStatus, str] = {
-    SyncToParentStatus.MergedToParent: AnsiEscapeCodes.DIM,
-    SyncToParentStatus.InSync: AnsiEscapeCodes.GREEN,
-    SyncToParentStatus.InSyncButForkPointOff: AnsiEscapeCodes.YELLOW,
-    SyncToParentStatus.OutOfSync: AnsiEscapeCodes.RED
-}
-
-sync_to_parent_status_to_junction_ascii_only_map: Dict[SyncToParentStatus, str] = {
-    SyncToParentStatus.MergedToParent: "m-",
-    SyncToParentStatus.InSync: "o-",
-    SyncToParentStatus.InSyncButForkPointOff: "?-",
-    SyncToParentStatus.OutOfSync: "x-"
-}
diff --git a/git_machete/code_hosting.py b/git_machete/code_hosting.py
index 0eb4ddb..6c138a3 100644
--- a/git_machete/code_hosting.py
+++ b/git_machete/code_hosting.py
@@ -89,6 +89,7 @@ class CodeHostingGitConfigKeys(NamedTuple):
     remote: str
     annotate_with_urls: str
     force_description_from_commit_message: str
+    pr_description_intro_style: str
 
     def for_locating_repo_message(self) -> str:
         return f"`{self.domain}`, `{self.organization}`, `{self.repository}`, `{self.remote}`"
diff --git a/git_machete/constants.py b/git_machete/constants.py
index b5cf433..c522a7f 100644
--- a/git_machete/constants.py
+++ b/git_machete/constants.py
@@ -1,49 +1,4 @@
-from enum import Enum, IntEnum
-from typing import Optional
-
-from git_machete.exceptions import MacheteException
-
 DISCOVER_DEFAULT_FRESH_BRANCH_COUNT = 10
 MAX_COMMITS_FOR_SQUASH_MERGE_DETECTION = 1000
 INITIAL_COMMIT_COUNT_FOR_LOG = 10
 TOTAL_COMMIT_COUNT_FOR_LOG = 100
-
-PICK_FIRST_ROOT: int = 0
-PICK_LAST_ROOT: int = -1
-
-
-class SyncToRemoteStatuses(IntEnum):
-    NO_REMOTES = 0
-    UNTRACKED = 1
-    IN_SYNC_WITH_REMOTE = 2
-    BEHIND_REMOTE = 3
-    AHEAD_OF_REMOTE = 4
-    DIVERGED_FROM_AND_OLDER_THAN_REMOTE = 5
-    DIVERGED_FROM_AND_NEWER_THAN_REMOTE = 6
-
-
-class GitFormatPatterns(Enum):
-    # %ai for ISO-8601 format
-    AUTHOR_DATE = "%ai"
-    # %aE/%aN (rather than %ae/%an) for respecting .mailmap; see `git rev-list --help`
-    AUTHOR_EMAIL = "%aE"
-    AUTHOR_NAME = "%aN"
-    # subject and body
-    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 7e58566..4a7e904 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -249,6 +249,16 @@ long_docs: Dict[str, str] = {
               Setting this config key to `true` will force `git machete github create-pr` to take PR description
               from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.github/pull_request_template.md` is present.
 
+           `machete.github.prDescriptionIntroStyle`:
+
+                 Select the style of the intro prepended to PR description:
+
+                 * `full`    — include both a chain of upstream PRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream PRs
+
+                 * `up-only` — default, include only a chain of upstream PRs
+
+                 * `none`    — prepend no intro to the PR description at all
+
            `machete.gitlab.{domain,remote,namespace,project}`:
 
                  GitLab self-managed domain
@@ -276,6 +286,16 @@ long_docs: Dict[str, str] = {
               Setting this config key to `true` will force `git machete gitlab create-mr` to take MR description
               from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.gitlab/merge_request_templates/Default.md` is present.
 
+           `machete.gitlab.mrDescriptionIntroStyle`:
+
+                 Select the style of the intro prepended to MR description:
+
+                 * `full`    — include both a chain of upstream MRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream MRs
+
+                 * `up-only` — default, include only a chain of upstream MRs
+
+                 * `none`    — prepend no intro to the MR description at all
+
            `machete.overrideForkPoint.<branch>.to`:
  
               Executing `git machete fork-point --override-to[-parent|-inferred|=<revision>] [<branch>]` sets up a fork point override for `<branch>`.
@@ -656,10 +676,10 @@ long_docs: Dict[str, str] = {
  
               Sets the base of the current (or specified) branch's PR to upstream (parent) branch, as seen by git machete (see `git machete show up`).
 
-              If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended with a section
-              listing the entire related chain of PRs.
+              If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended
+              with an intro section listing the entire related chain of PRs.
 
-              This header will be updated or removed accordingly with the subsequent runs of `retarget-pr`.
+              This intro will be updated or removed accordingly with the subsequent runs of `retarget-pr`, even if the base branch is already up to date.
 
               <b>Options:</b>
 
@@ -715,6 +735,16 @@ long_docs: Dict[str, str] = {
               Setting this config key to `true` will force `git machete github create-pr` to take PR description
               from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.github/pull_request_template.md` is present.
 
+           `machete.github.prDescriptionIntroStyle` (`create-pr`, `restack-pr` and `retarget-pr`):
+
+                 Select the style of the intro prepended to PR description:
+
+                 * `full`    — include both a chain of upstream PRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream PRs
+
+                 * `up-only` — default, include only a chain of upstream PRs
+
+                 * `none`    — prepend no intro to the PR description at all
+
         <b>Environment variables (all subcommands):</b>
            `GITHUB_TOKEN`
               GitHub API token.
@@ -728,7 +758,7 @@ long_docs: Dict[str, str] = {
 
         Creates, checks out and manages GitLab MRs while keeping them reflected in branch layout file.
 
-        To allow GitLab API access for private repositories (and also to perform side-effecting actions like opening a PR,
+        To allow GitLab API access for private projects (and also to perform side-effecting actions like opening a MR,
         even in case of public projects), a GitLab API token with `api` scope is required, see https://gitlab.com/-/user_settings/personal_access_tokens.
         This will be resolved from the first of:
            * `GITLAB_TOKEN` env var,
@@ -765,9 +795,9 @@ long_docs: Dict[str, str] = {
 
            `checkout-mrs [--all | --by=<gitlab-login> | --mine | <MR-number-1> ... <MR-number-N>]`:
  
-              Check out the source branch of the given pull requests (specified by numbers or by a flag),
-              also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally.
-              Once the specified pull requests are checked out locally, annotate local branches with corresponding pull request numbers.
+              Check out the source branch of the given merge requests (specified by numbers or by a flag),
+              also traverse chain of merge requests upwards, adding branches one by one to git-machete and check them out locally.
+              Once the specified merge requests are checked out locally, annotate local branches with corresponding merge request numbers.
               If only one MR has been checked out, then switch the local repository's HEAD to its source branch.
 
               When the current user is NOT the author of the MR associated with that branch, adds `rebase=no push=no` branch qualifiers used by `git machete traverse`,
@@ -787,7 +817,7 @@ long_docs: Dict[str, str] = {
 
               <b>Parameters:</b>
 
-              `<MR-number-1> ... <MR-number-N>`    Pull request numbers to checkout.
+              `<MR-number-1> ... <MR-number-N>`    Merge request numbers to checkout.
 
            `create-mr [--draft] [--title=<title>] [--yes]`:
  
@@ -837,10 +867,10 @@ long_docs: Dict[str, str] = {
  
               Sets the target of the current (or specified) branch's MR to upstream (parent) branch, as seen by git machete (see `git machete show up`).
 
-              If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended with a section
-              listing the entire related chain of MRs.
+              If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended
+              with an intro section listing the entire related chain of MRs.
 
-              This header will be updated or removed accordingly with the subsequent runs of `retarget-mr`.
+              This intro will be updated or removed accordingly with the subsequent runs of `retarget-mr`, even if the target branch is already up to date.
 
               <b>Options:</b>
 
@@ -883,6 +913,16 @@ long_docs: Dict[str, str] = {
               Setting this config key to `true` will force `git machete gitlab create-mr` to take MR description
               from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.gitlab/merge_request_templates/Default.md` is present.
 
+           `machete.gitlab.mrDescriptionIntroStyle` (`create-mr`, `restack-mr` and `retarget-mr`):
+
+                 Select the style of the intro prepended to MR description:
+
+                 * `full`    — include both a chain of upstream MRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream MRs
+
+                 * `up-only` — default, include only a chain of upstream MRs
+
+                 * `none`    — prepend no intro to the MR description at all
+
         <b>Environment variables (all subcommands):</b>
            `GITLAB_TOKEN`
               GitLab API token.
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 4b22991..29cc057 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -3,13 +3,13 @@ import os
 import re
 import string
 import sys
+from enum import Enum, auto
 from pathlib import Path
 from typing import (Any, Dict, Iterator, List, Match, NamedTuple, Optional,
                     Set, Tuple)
 
 from . import utils
-from .constants import (MAX_COMMITS_FOR_SQUASH_MERGE_DETECTION,
-                        GitFormatPatterns, SyncToRemoteStatuses)
+from .constants import MAX_COMMITS_FOR_SQUASH_MERGE_DETECTION
 from .exceptions import UnderlyingGitException, UnexpectedMacheteException
 from .utils import (AnsiEscapeCodes, CommandResult, colored, debug, fmt,
                     hex_repr)
@@ -191,6 +191,28 @@ HEAD = AnyRevision.of("HEAD")
 GIT_EXEC = ("git", "-c", "log.showSignature=false")
 
 
+class SyncToRemoteStatus(Enum):
+    NO_REMOTES = auto()
+    UNTRACKED = auto()
+    IN_SYNC_WITH_REMOTE = auto()
+    BEHIND_REMOTE = auto()
+    AHEAD_OF_REMOTE = auto()
+    DIVERGED_FROM_AND_OLDER_THAN_REMOTE = auto()
+    DIVERGED_FROM_AND_NEWER_THAN_REMOTE = auto()
+
+
+class GitFormatPatterns(Enum):
+    # %ai for ISO-8601 format
+    AUTHOR_DATE = "%ai"
+    # %aE/%aN (rather than %ae/%an) for respecting .mailmap; see `git rev-list --help`
+    AUTHOR_EMAIL = "%aE"
+    AUTHOR_NAME = "%aN"
+    # subject and body
+    FULL_MESSAGE = "%B"
+    # subject NOT included
+    MESSAGE_BODY = "%b"
+
+
 class GitContext:
 
     def __init__(self) -> None:
@@ -322,6 +344,10 @@ class GitContext:
                 else:
                     raise UnexpectedMacheteException(f"Cannot parse config entry: {config_entry}.")
 
+    def get_config_attr(self, key: str, default_value: str) -> str:
+        value = self.get_config_attr_or_none(key)
+        return default_value if value is None else value
+
     def get_config_attr_or_none(self, key: str) -> Optional[str]:
         self.__ensure_config_loaded()
         assert self.__config_cached is not None
@@ -998,25 +1024,25 @@ class GitContext:
             utils.get_non_empty_lines(self._popen_git("log", "--format=%H:%h:%s", f"^{earliest_exclusive}", latest_inclusive, "--").stdout)
         ))))
 
-    def get_relation_to_remote_counterpart(self, branch: LocalBranchShortName, remote_branch: RemoteBranchShortName) -> int:
+    def get_relation_to_remote_counterpart(self, branch: LocalBranchShortName, remote_branch: RemoteBranchShortName) -> SyncToRemoteStatus:
         b_is_ancestor_of_rb = self.is_ancestor_or_equal(branch.full_name(), remote_branch.full_name())
         rb_is_ancestor_of_b = self.is_ancestor_or_equal(remote_branch.full_name(), branch.full_name())
         if b_is_ancestor_of_rb:
-            return SyncToRemoteStatuses.IN_SYNC_WITH_REMOTE if rb_is_ancestor_of_b else SyncToRemoteStatuses.BEHIND_REMOTE
+            return SyncToRemoteStatus.IN_SYNC_WITH_REMOTE if rb_is_ancestor_of_b else SyncToRemoteStatus.BEHIND_REMOTE
         elif rb_is_ancestor_of_b:
-            return SyncToRemoteStatuses.AHEAD_OF_REMOTE
+            return SyncToRemoteStatus.AHEAD_OF_REMOTE
         else:
             b_t = self.get_committer_unix_timestamp_by_revision(branch)
             rb_t = self.get_committer_unix_timestamp_by_revision(remote_branch)
-            return SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE if b_t < rb_t else \
-                SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE
+            return SyncToRemoteStatus.DIVERGED_FROM_AND_OLDER_THAN_REMOTE if b_t < rb_t else \
+                SyncToRemoteStatus.DIVERGED_FROM_AND_NEWER_THAN_REMOTE
 
-    def get_combined_remote_sync_status(self, branch: LocalBranchShortName) -> Tuple[int, Optional[str]]:
+    def get_combined_remote_sync_status(self, branch: LocalBranchShortName) -> Tuple[SyncToRemoteStatus, Optional[str]]:
         if not self.get_remotes():
-            return SyncToRemoteStatuses.NO_REMOTES, None
+            return SyncToRemoteStatus.NO_REMOTES, None
         remote_branch = self.get_combined_counterpart_for_fetching_of_branch(branch)
         if not remote_branch:
-            return SyncToRemoteStatuses.UNTRACKED, None
+            return SyncToRemoteStatus.UNTRACKED, None
         return self.get_relation_to_remote_counterpart(branch, remote_branch), self.get_combined_remote_for_fetching_of_branch(branch)
 
     def get_latest_checkout_timestamps(self) -> Dict[str, int]:  # TODO (#110): default dict with 0
diff --git a/git_machete/github.py b/git_machete/github.py
index a76f7ae..925d701 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -179,6 +179,7 @@ class GitHubClient(CodeHostingClient):
                 remote='machete.github.remote',
                 annotate_with_urls='machete.github.annotateWithUrls',
                 force_description_from_commit_message='machete.github.forceDescriptionFromCommitMessage',
+                pr_description_intro_style='machete.github.prDescriptionIntroStyle',
             )
         )
 
@@ -247,7 +248,7 @@ class GitHubClient(CodeHostingClient):
                          "Skipped adding reviewers to the pull request.")
                 else:
                     raise UnexpectedMacheteException(
-                        f'GitLab API returned 422 (Unprocessable Entity) HTTP status with error message: `{error_reason}`.')
+                        f'GitHub API returned 422 (Unprocessable Entity) HTTP status with error message: `{error_reason}`.')
             elif err.code in (http.HTTPStatus.UNAUTHORIZED, http.HTTPStatus.FORBIDDEN):
                 first_line = f'GitHub API returned `{err.code}` HTTP status with error message: `{err.reason}`\n'
                 last_line = 'You can also use a different token provider - see `git machete help github` for details.'
diff --git a/git_machete/gitlab.py b/git_machete/gitlab.py
index ede15b6..93f7feb 100644
--- a/git_machete/gitlab.py
+++ b/git_machete/gitlab.py
@@ -134,6 +134,7 @@ class GitLabClient(CodeHostingClient):
                 remote='machete.gitlab.remote',
                 annotate_with_urls='machete.gitlab.annotateWithUrls',
                 force_description_from_commit_message='machete.gitlab.forceDescriptionFromCommitMessage',
+                pr_description_intro_style='machete.gitlab.mrDescriptionIntroStyle',
             )
         )
 
@@ -215,7 +216,7 @@ class GitLabClient(CodeHostingClient):
                         f"ended up in `{error_response.get('message')}` response from GitLab.\n"
                         "Please report this error as a comment under `https://github.com/VirtusLab/git-machete/issues/1212`.\n"
                         "As a workaround for now, please check `git remote -v`.\n"
-                        "Most likely you use an old URL of a repository that has been moved since.\n"
+                        "Most likely you use an old URL of a project that has been moved since.\n"
                         "Use `git remote set-url <remote> <URL>` to update the URL.")
                 else:
                     UnexpectedMacheteException(
diff --git a/requirements/pypi-deploy.txt b/requirements/pypi-deploy.txt
index c3dee8b..351d921 100644
--- a/requirements/pypi-deploy.txt
+++ b/requirements/pypi-deploy.txt
@@ -1,3 +1,3 @@
-setuptools==72.1.0
+setuptools==72.2.0
 twine==5.1.1
 wheel==0.44.0
diff --git a/tox.ini b/tox.ini
index 0f846dc..4e215d3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -17,9 +17,12 @@ install_command =
 deps =
   -r{[requirements]dir}/testenv.txt
 allowlist_externals = mkdir
+setenv =
+  PYTHONWARNINGS=ignore:datetime.datetime.utcfromtimestamp() is deprecated
 commands =
   mkdir -p test-results/
-  pytest --numprocesses=auto --junitxml=test-results/testenv-{envname}.xml -m "not completion_e2e" {posargs}
+  # `python -m pytest` so that the local plugin can be loaded, see https://stackoverflow.com/a/48306599
+  python -m pytest -p tests.pytest_full_operands --numprocesses=auto --junitxml=test-results/testenv-{envname}.xml -m "not completion_e2e" {posargs}
 
 [testenv:test-completions]
 description = "Test shell completions"
@@ -75,7 +78,7 @@ commands = isort --check-only .
 description = "Run `vulture` static code analyzer to detect unused code"
 deps =
   -r{[requirements]dir}/vulture-check.txt
-commands = vulture --ignore-names check_hostname,verify_mode git_machete/ tests/
+commands = vulture --ignore-names check_hostname,verify_mode,pytest_* git_machete/ tests/
 
 [testenv:venv]
 commands = {posargs}


Test patch:
diff --git a/requirements/testenv.txt b/requirements/testenv.txt
index 22c9d5c..321a462 100644
--- a/requirements/testenv.txt
+++ b/requirements/testenv.txt
@@ -2,6 +2,8 @@ pytest==7.0.1; python_version <= '3.6'
 pytest==7.4.4; python_version >= '3.7' and python_version < '3.8'
 pytest==8.3.2; python_version >= '3.8'
 
+pytest-clarity==1.0.1
+
 pytest-mock==3.6.1; python_version <= '3.6'
 pytest-mock==3.11.1; python_version >= '3.7' and python_version < '3.8'
 pytest-mock==3.14.0; python_version >= '3.8'
diff --git a/tests/mockers_gitlab.py b/tests/mockers_gitlab.py
index 163b88c..568980d 100644
--- a/tests/mockers_gitlab.py
+++ b/tests/mockers_gitlab.py
@@ -28,7 +28,7 @@ def mock_mr_json(head: str, base: str, number: int,
                  draft: bool = False
                  ) -> Dict[str, Any]:
     return {
-        'title': 'Draft: PR title' if draft else 'PR title',
+        'title': 'Draft: MR title' if draft else 'MR title',
         'source_branch': head,
         'source_project_id': repo_id,
         'target_branch': base,
@@ -143,7 +143,7 @@ def __mock_urlopen_impl(gitlab_api_state: MockGitLabAPIState, request: Request)
             head: Optional[str] = query_params.get('source_branch')
             if head:
                 mrs = gitlab_api_state.get_open_mrs_by_head(head)
-                # If no matching PRs are found, the real GitLab returns 200 OK with an empty JSON array - not 404.
+                # If no matching MRs are found, the real GitLab returns 200 OK with an empty JSON array - not 404.
                 return MockAPIResponse(HTTPStatus.OK, mrs)
             else:
                 mrs = gitlab_api_state.get_open_mrs()
diff --git a/tests/pytest_full_operands.py b/tests/pytest_full_operands.py
new file mode 100644
index 0000000..6de3672
--- /dev/null
+++ b/tests/pytest_full_operands.py
@@ -0,0 +1,34 @@
+# mypy: ignore-errors
+
+
+import pytest
+
+OPTION = 'full-operands'
+
+
+def pytest_addoption(parser):
+    parser.getgroup(OPTION).addoption(
+        f'--{OPTION}',
+        action='store_true',
+        dest=OPTION,
+        default=False,
+        help='Always show full operands on `assert x == y` failures'
+    )
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_assertrepr_compare(config, op: str, left, right):
+    if op == "==" and config.getoption(OPTION):
+        def lines_for(arg):
+            return [''] + ['  ' + x for x in arg.splitlines()] + ['']
+        return [
+            'Comparing values:',
+            'left (typically meaning actual):', *lines_for(left),
+            'right (typically meaning expected):', *lines_for(right)
+        ]
+
+
+def pytest_configure(config):
+    if config.getoption(OPTION):
+        # Set verbosity level to the equivalent of '-vv'
+        config.option.verbose = 2
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index 332e579..db3ef23 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -159,12 +159,13 @@ class TestGitHubCreatePR(BaseTest):
 
             # Based on PR #3
 
-            ## Full chain of PRs as of 2023-12-31
+            ## Chain of upstream PRs as of 2023-12-31
 
-            * PR #5:
-              `chore/fields` ➔ `ignore-trailing`
             * PR #3:
-              `ignore-trailing` ➔ `hotfix/add-trigger`
+              `hotfix/add-trigger` ← `ignore-trailing`
+
+              * **PR #5 (THIS ONE)**:
+                `ignore-trailing` ← `chore/fields`
 
             <!-- end git-machete generated -->
 
@@ -364,14 +365,16 @@ class TestGitHubCreatePR(BaseTest):
 
             # Based on PR #2
 
-            ## Full chain of PRs as of 2023-12-31
+            ## Chain of upstream PRs as of 2023-12-31
 
-            * PR #3:
-              `call-ws` ➔ `build-chain`
-            * PR #2:
-              `build-chain` ➔ `allow-ownership-link`
             * PR #1:
-              `allow-ownership-link` ➔ `develop`
+              `develop` ← `allow-ownership-link`
+
+              * PR #2:
+                `allow-ownership-link` ← `build-chain`
+
+                * **PR #3 (THIS ONE)**:
+                  `build-chain` ← `call-ws`
 
             <!-- end git-machete generated -->
         ''')[1:]
@@ -386,16 +389,19 @@ class TestGitHubCreatePR(BaseTest):
 
             # Based on PR #3
 
-            ## Full chain of PRs as of 2023-12-31
+            ## Chain of upstream PRs as of 2023-12-31
 
-            * PR #4:
-              `drop-constraint` ➔ `call-ws`
-            * PR #3:
-              `call-ws` ➔ `build-chain`
-            * PR #2:
-              `build-chain` ➔ `allow-ownership-link`
             * PR #1:
-              `allow-ownership-link` ➔ `develop`
+              `develop` ← `allow-ownership-link`
+
+              * PR #2:
+                `allow-ownership-link` ← `build-chain`
+
+                * PR #3:
+                  `build-chain` ← `call-ws`
+
+                  * **PR #4 (THIS ONE)**:
+                    `call-ws` ← `drop-constraint`
 
             <!-- end git-machete generated -->
 
@@ -598,12 +604,13 @@ class TestGitHubCreatePR(BaseTest):
 
             # Based on PR #15
 
-            ## Full chain of PRs as of 2023-12-31
+            ## Chain of upstream PRs as of 2023-12-31
 
-            * PR #16:
-              `feature` ➔ `branch-1`
             * PR #15:
-              `branch-1` ➔ `root`
+              `root` ← `branch-1`
+
+              * **PR #16 (THIS ONE)**:
+                `branch-1` ← `feature`
 
             <!-- end git-machete generated -->
 
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index 5b1a1bd..78ac32c 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -19,7 +19,6 @@ class TestGitHubRetargetPR(BaseTest):
             mock_pr_json(head='feature_2', base='master', number=25, body=None),
             mock_pr_json(head='feature_3', base='master', number=30),
             mock_pr_json(head='feature_4', base='feature', number=35),
-            # Let's include another PR for `feature_2`, but with a different base branch
             mock_pr_json(head='feature_4', base='feature', number=40),
         )
 
@@ -265,12 +264,13 @@ class TestGitHubRetargetPR(BaseTest):
 
             # Based on PR #15
 
-            ## Full chain of PRs as of 2023-12-31
+            ## Chain of upstream PRs as of 2023-12-31
 
-            * PR #20:
-              `feature_1` ➔ `feature`
             * PR #15:
-              `feature` ➔ `master`
+              `master` ← `feature`
+
+              * **PR #20 (THIS ONE)**:
+                `feature` ← `feature_1`
 
             <!-- end git-machete generated -->
             # Summary''')[1:]
@@ -278,8 +278,8 @@ class TestGitHubRetargetPR(BaseTest):
         # branch feature_2 is not present in any of the remotes
         (
             self.repo_sandbox.check_out('feature')
-                .new_branch('feature_2')
-                .commit('introduce feature 2')
+            .new_branch('feature_2')
+            .commit('introduce feature 2')
         )
 
         body = \
@@ -297,31 +297,19 @@ class TestGitHubRetargetPR(BaseTest):
         # branch feature_2 present in only one remote: origin_1 and there is no tracking data available -> infer the remote
         (
             self.repo_sandbox.check_out('feature_2')
-                .push(remote='origin_1', set_upstream=False)
+            .push(remote='origin_1', set_upstream=False)
         )
 
+        self.repo_sandbox.set_git_config_key("machete.github.prDescriptionIntroStyle", "none")
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #25 has been switched to feature\n'
-            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
             'Description of PR #25 has been updated\n'
         )
         pr25 = github_api_state.get_pull_by_number(25)
         assert pr25 is not None
         assert pr25['base']['ref'] == 'feature'
-        assert pr25['body'] == textwrap.dedent('''
-            <!-- start git-machete generated -->
-
-            # Based on PR #15
-
-            ## Full chain of PRs as of 2023-12-31
-
-            * PR #25:
-              `feature_2` ➔ `feature`
-            * PR #15:
-              `feature` ➔ `master`
-
-            <!-- end git-machete generated -->''')[1:]
+        assert pr25['body'] == ''
 
         # branch feature_3 present in only one remote: origin_1 and has tracking data
         (
@@ -342,6 +330,7 @@ class TestGitHubRetargetPR(BaseTest):
             """
         rewrite_branch_layout_file(body)
 
+        self.repo_sandbox.unset_git_config_key("machete.github.prDescriptionIntroStyle")
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #30 has been switched to feature_2\n'
@@ -356,14 +345,16 @@ class TestGitHubRetargetPR(BaseTest):
 
             # Based on PR #25
 
-            ## Full chain of PRs as of 2023-12-31
+            ## Chain of upstream PRs as of 2023-12-31
 
-            * PR #30:
-              `feature_3` ➔ `feature_2`
-            * PR #25:
-              `feature_2` ➔ `feature`
             * PR #15:
-              `feature` ➔ `master`
+              `master` ← `feature`
+
+              * PR #25:
+                `feature` ← `feature_2`
+
+                * **PR #30 (THIS ONE)**:
+                  `feature_2` ← `feature_3`
 
             <!-- end git-machete generated -->
             # Summary''')[1:]
@@ -393,12 +384,13 @@ class TestGitHubRetargetPR(BaseTest):
 
             # Based on PR #15
 
-            ## Full chain of PRs as of 2023-12-31
+            ## Chain of upstream PRs as of 2023-12-31
 
-            * PR #30:
-              `feature_3` ➔ `feature`
             * PR #15:
-              `feature` ➔ `master`
+              `master` ← `feature`
+
+              * **PR #30 (THIS ONE)**:
+                `feature` ← `feature_3`
 
             <!-- end git-machete generated -->
             # Summary''')[1:]
@@ -424,6 +416,39 @@ class TestGitHubRetargetPR(BaseTest):
         assert pr30['base']['ref'] == 'root'
         assert pr30['body'] == '# Summary'
 
+        self.repo_sandbox.check_out('feature')
+        self.repo_sandbox.remove_remote('origin_2')
+        self.repo_sandbox.set_git_config_key("machete.github.prDescriptionIntroStyle", "full")
+
+        assert_success(
+            ['github', 'retarget-pr'],
+            'Base branch of PR #15 has been switched to branch-1\n'
+            'Checking for open GitHub PRs (to determine PR tree)... OK\n'
+            'Description of PR #15 has been updated\n'
+        )
+        pr15 = github_api_state.get_pull_by_number(15)
+        assert pr15 is not None
+        assert pr15['base']['ref'] == 'branch-1'
+        assert pr15['body'] == textwrap.dedent('''
+            <!-- start git-machete generated -->
+
+            ## Chain of upstream PRs & tree of downstream PRs as of 2023-12-31
+
+            * **PR #15 (THIS ONE)**:
+              `branch-1` ← `feature`
+
+                * PR #20:
+                  `feature` ← `feature_1`
+
+                * PR #25:
+                  `feature` ← `feature_2`
+
+                * PR #35:
+                  `feature` ← `feature_4`
+
+            <!-- end git-machete generated -->
+            # Summary''')[1:]
+
     @staticmethod
     def github_api_state_for_test_retarget_pr_root_branch() -> MockGitHubAPIState:
         return MockGitHubAPIState.with_prs(
diff --git a/tests/test_gitlab.py b/tests/test_gitlab.py
index aa171b7..e426576 100644
--- a/tests/test_gitlab.py
+++ b/tests/test_gitlab.py
@@ -40,14 +40,14 @@ class TestGitLab(BaseTest):
             assert org_and_repo.organization == organization
             assert org_and_repo.repository == repository
 
-    PR_COUNT_FOR_TEST_GITLAB_API_PAGINATION = 12
+    MR_COUNT_FOR_TEST_GITLAB_API_PAGINATION = 12
 
     @staticmethod
     def gitlab_api_state_for_test_gitlab_api_pagination() -> MockGitLabAPIState:
         return MockGitLabAPIState.with_mrs(*[
             mock_mr_json(
                 head=f'feature_{i:02d}', base='develop', number=i
-            ) for i in range(TestGitLab.PR_COUNT_FOR_TEST_GITLAB_API_PAGINATION)])
+            ) for i in range(TestGitLab.MR_COUNT_FOR_TEST_GITLAB_API_PAGINATION)])
 
     def test_gitlab_api_pagination(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'builtins.input', mock_input_returning_y)
@@ -61,14 +61,14 @@ class TestGitLab(BaseTest):
             .commit("first commit")
             .push()
         )
-        for i in range(self.PR_COUNT_FOR_TEST_GITLAB_API_PAGINATION):
+        for i in range(self.MR_COUNT_FOR_TEST_GITLAB_API_PAGINATION):
             self.repo_sandbox.check_out('develop').new_branch(f'feature_{i:02d}').commit().push()
         self.repo_sandbox.check_out('develop')
-        body: str = 'develop *\n' + '\n'.join([f'feature_{i:02d}' for i in range(self.PR_COUNT_FOR_TEST_GITLAB_API_PAGINATION)]) + '\n'
+        body: str = 'develop *\n' + '\n'.join([f'feature_{i:02d}' for i in range(self.MR_COUNT_FOR_TEST_GITLAB_API_PAGINATION)]) + '\n'
         rewrite_branch_layout_file(body)
 
         self.repo_sandbox.check_out('develop')
-        for i in range(self.PR_COUNT_FOR_TEST_GITLAB_API_PAGINATION):
+        for i in range(self.MR_COUNT_FOR_TEST_GITLAB_API_PAGINATION):
             self.repo_sandbox.delete_branch(f"feature_{i:02d}")
         body = 'develop *\n'
         rewrite_branch_layout_file(body)
@@ -76,7 +76,7 @@ class TestGitLab(BaseTest):
         launch_command('gitlab', 'checkout-mrs', '--all')
         launch_command('discover', '--checked-out-since=1 day ago')
         expected_status_output = 'develop *\n' + '\n'.join([f'|\no-feature_{i:02d}  rebase=no push=no'
-                                                            for i in range(self.PR_COUNT_FOR_TEST_GITLAB_API_PAGINATION)]) + '\n'
+                                                            for i in range(self.MR_COUNT_FOR_TEST_GITLAB_API_PAGINATION)]) + '\n'
         assert_success(['status'], expected_status_output)
 
     def test_gitlab_enterprise_domain_unauthorized_without_token(self, mocker: MockerFixture) -> None:
@@ -166,7 +166,7 @@ class TestGitLab(BaseTest):
                     'but no remote seems to correspond to example-org/example-repo (namespace/project) on GitLab.\n'
                     'Consider pointing to the remote via machete.gitlab.remote config key')
 
-        self.repo_sandbox.add_remote("new-origin", "https://github.com/example-org/example-repo.git")  # not a valid GitLab repo URL
+        self.repo_sandbox.add_remote("new-origin", "https://github.com/example-org/example-repo.git")  # not a valid GitLab project URL
         with git_config_key('machete.gitlab.remote', "new-origin"):
             assert_failure(
                 ['gitlab', 'checkout-mrs', '--all'],
diff --git a/tests/test_gitlab_checkout_mrs.py b/tests/test_gitlab_checkout_mrs.py
index 09480aa..a9c35e1 100644
--- a/tests/test_gitlab_checkout_mrs.py
+++ b/tests/test_gitlab_checkout_mrs.py
@@ -112,7 +112,7 @@ class TestGitLabCheckoutMRs(BaseTest):
             """
         rewrite_branch_layout_file(body)
 
-        # not broken chain of pull requests (root found in dependency tree)
+        # not broken chain of merge requests (root found in dependency tree)
         launch_command('gitlab', 'checkout-mrs', '18')
         assert_success(
             ["status"],
@@ -138,7 +138,7 @@ class TestGitLabCheckoutMRs(BaseTest):
                     o-chore/redundant_checks *  MR !18 (some_other_user) rebase=no push=no
             """
         )
-        # broken chain of pull requests (add new root)
+        # broken chain of merge requests (add new root)
         launch_command('gitlab', 'checkout-mrs', '24')
         assert_success(
             ["status"],
@@ -171,7 +171,7 @@ class TestGitLabCheckoutMRs(BaseTest):
             """
         )
 
-        # broken chain of pull requests (branches already added)
+        # broken chain of merge requests (branches already added)
         launch_command('gitlab', 'checkout-mrs', '24')
         assert_success(
             ["status"],
@@ -253,7 +253,7 @@ class TestGitLabCheckoutMRs(BaseTest):
         expected_msg = "Checking for open GitLab MRs... OK\n"
         assert_success(['gitlab', 'checkout-mrs', '--by', 'some_other_user'], expected_msg)
 
-        # Check against closed pull request with source branch deleted from remote
+        # Check against closed merge request with source branch deleted from remote
         self.repo_sandbox.create_repo("other-local", bare=False, switch_dir_to_new_repo=True)
         (
             self.repo_sandbox
diff --git a/tests/test_gitlab_create_mr.py b/tests/test_gitlab_create_mr.py
index 63e51f0..4578ea4 100644
--- a/tests/test_gitlab_create_mr.py
+++ b/tests/test_gitlab_create_mr.py
@@ -158,12 +158,13 @@ class TestGitLabCreateMR(BaseTest):
 
             # Based on MR !3
 
-            ## Full chain of MRs as of 2023-12-31
+            ## Chain of upstream MRs as of 2023-12-31
 
-            * MR !5:
-              `chore/fields` ➔ `ignore-trailing`
             * MR !3:
-              `ignore-trailing` ➔ `hotfix/add-trigger`
+              `hotfix/add-trigger` ← `ignore-trailing`
+
+              * **MR !5 (THIS ONE)**:
+                `ignore-trailing` ← `chore/fields`
 
             <!-- end git-machete generated -->
 
@@ -350,47 +351,37 @@ class TestGitLabCreateMR(BaseTest):
             """
         rewrite_branch_layout_file(body)
 
+        self.repo_sandbox.check_out("drop-constraint")
+        launch_command("gitlab", "create-mr", "--yes")
+        pr = gitlab_api_state.get_mr_by_number(3)
+        assert pr is not None
+        assert pr['description'] == ''  # no chain at this moment
+
+        self.repo_sandbox.write_to_file(".gitlab/merge_request_templates/Default.md", "# MR title\n## Summary\n## Test plan\n")
+        self.repo_sandbox.set_git_config_key("machete.gitlab.mrDescriptionIntroStyle", "full")
+
         self.repo_sandbox.check_out("call-ws")
         launch_command("gitlab", "create-mr")
-        pr = gitlab_api_state.get_mr_by_number(3)
+        pr = gitlab_api_state.get_mr_by_number(4)
         assert pr is not None
         assert pr['description'] == textwrap.dedent('''
             <!-- start git-machete generated -->
 
             # Based on MR !2
 
-            ## Full chain of MRs as of 2023-12-31
+            ## Chain of upstream MRs & tree of downstream MRs as of 2023-12-31
 
-            * MR !3:
-              `call-ws` ➔ `build-chain`
-            * MR !2:
-              `build-chain` ➔ `allow-ownership-link`
             * MR !1:
-              `allow-ownership-link` ➔ `develop`
-
-            <!-- end git-machete generated -->
-        ''')[1:]
+              `develop` ← `allow-ownership-link`
 
-        self.repo_sandbox.write_to_file(".gitlab/merge_request_templates/Default.md", "# MR title\n## Summary\n## Test plan\n")
-        self.repo_sandbox.check_out("drop-constraint")
-        launch_command("gitlab", "create-mr", "--yes")
-        pr = gitlab_api_state.get_mr_by_number(4)
-        assert pr is not None
-        assert pr['description'] == textwrap.dedent('''
-            <!-- start git-machete generated -->
-
-            # Based on MR !3
+              * MR !2:
+                `allow-ownership-link` ← `build-chain`
 
-            ## Full chain of MRs as of 2023-12-31
+                * **MR !4 (THIS ONE)**:
+                  `build-chain` ← `call-ws`
 
-            * MR !4:
-              `drop-constraint` ➔ `call-ws`
-            * MR !3:
-              `call-ws` ➔ `build-chain`
-            * MR !2:
-              `build-chain` ➔ `allow-ownership-link`
-            * MR !1:
-              `allow-ownership-link` ➔ `develop`
+                    * MR !3:
+                      `call-ws` ← `drop-constraint`
 
             <!-- end git-machete generated -->
 
@@ -593,12 +584,13 @@ class TestGitLabCreateMR(BaseTest):
 
             # Based on MR !15
 
-            ## Full chain of MRs as of 2023-12-31
+            ## Chain of upstream MRs as of 2023-12-31
 
-            * MR !16:
-              `feature` ➔ `branch-1`
             * MR !15:
-              `branch-1` ➔ `root`
+              `root` ← `branch-1`
+
+              * **MR !16 (THIS ONE)**:
+                `branch-1` ← `feature`
 
             <!-- end git-machete generated -->
 
diff --git a/tests/test_gitlab_retarget_mr.py b/tests/test_gitlab_retarget_mr.py
index 61c43d8..c85945c 100644
--- a/tests/test_gitlab_retarget_mr.py
+++ b/tests/test_gitlab_retarget_mr.py
@@ -19,7 +19,6 @@ class TestGitLabRetargetMR(BaseTest):
             mock_mr_json(head='feature_2', base='master', number=25, body=None),
             mock_mr_json(head='feature_3', base='master', number=30),
             mock_mr_json(head='feature_4', base='feature', number=35),
-            # Let's include another MR for `feature_2`, but with a different target branch
             mock_mr_json(head='feature_4', base='feature', number=40),
         )
 
@@ -75,7 +74,7 @@ class TestGitLabRetargetMR(BaseTest):
             ended up in Non GET methods are not allowed for moved projects response from GitLab.
             Please report this error as a comment under https://github.com/VirtusLab/git-machete/issues/1212.
             As a workaround for now, please check git remote -v.
-            Most likely you use an old URL of a repository that has been moved since.
+            Most likely you use an old URL of a project that has been moved since.
             Use git remote set-url <remote> <URL> to update the URL."""
         )
 
@@ -271,12 +270,13 @@ class TestGitLabRetargetMR(BaseTest):
 
             # Based on MR !15
 
-            ## Full chain of MRs as of 2023-12-31
+            ## Chain of upstream MRs as of 2023-12-31
 
-            * MR !20:
-              `feature_1` ➔ `feature`
             * MR !15:
-              `feature` ➔ `master`
+              `master` ← `feature`
+
+              * **MR !20 (THIS ONE)**:
+                `feature` ← `feature_1`
 
             <!-- end git-machete generated -->
             # Summary''')[1:]
@@ -320,12 +320,13 @@ class TestGitLabRetargetMR(BaseTest):
 
             # Based on MR !15
 
-            ## Full chain of MRs as of 2023-12-31
+            ## Chain of upstream MRs as of 2023-12-31
 
-            * MR !25:
-              `feature_2` ➔ `feature`
             * MR !15:
-              `feature` ➔ `master`
+              `master` ← `feature`
+
+              * **MR !25 (THIS ONE)**:
+                `feature` ← `feature_2`
 
             <!-- end git-machete generated -->''')[1:]
 
@@ -362,14 +363,16 @@ class TestGitLabRetargetMR(BaseTest):
 
             # Based on MR !25
 
-            ## Full chain of MRs as of 2023-12-31
+            ## Chain of upstream MRs as of 2023-12-31
 
-            * MR !30:
-              `feature_3` ➔ `feature_2`
-            * MR !25:
-              `feature_2` ➔ `feature`
             * MR !15:
-              `feature` ➔ `master`
+              `master` ← `feature`
+
+              * MR !25:
+                `feature` ← `feature_2`
+
+                * **MR !30 (THIS ONE)**:
+                  `feature_2` ← `feature_3`
 
             <!-- end git-machete generated -->
             # Summary''')[1:]
@@ -399,12 +402,13 @@ class TestGitLabRetargetMR(BaseTest):
 
             # Based on MR !15
 
-            ## Full chain of MRs as of 2023-12-31
+            ## Chain of upstream MRs as of 2023-12-31
 
-            * MR !30:
-              `feature_3` ➔ `feature`
             * MR !15:
-              `feature` ➔ `master`
+              `master` ← `feature`
+
+              * **MR !30 (THIS ONE)**:
+                `feature` ← `feature_3`
 
             <!-- end git-machete generated -->
             # Summary''')[1:]