Repository: VirtusLab/git-machete
Pull request #1306 (2024-08-19 20:38:56)

Display downstream PRs/MRs in PR/MR description


Patch:
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 e6049a0..7f43e8d 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,6 +1,11 @@
 # Release notes
 
-## New in git-machete 3.28.1
+## 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
 
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-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 c947c8a..00dd902 100755
--- a/ci/checks/run-all-checks.sh
+++ b/ci/checks/run-all-checks.sh
@@ -31,6 +31,8 @@ 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
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 07a7909..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 22, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Aug 23, 2024" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.28.1
+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
@@ -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 535ef7b..947444b 100644
--- a/docs/source/cli/gitlab.rst
+++ b/docs/source/cli/gitlab.rst
@@ -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 d5631d7..c9c9832 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.28.1'
+__version__ = '3.29.0'
diff --git a/git_machete/client.py b/git_machete/client.py
index c0d951c..4299f49 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -8,7 +8,7 @@ import sys
 import textwrap
 from collections import OrderedDict
 from enum import Enum, auto
-from typing import (Callable, Dict, Iterator, List, Optional, Tuple, Type,
+from typing import (Callable, Dict, Iterator, List, Optional, Set, Tuple, Type,
                     TypeVar)
 
 from . import git_config_keys, utils
@@ -63,9 +63,10 @@ class ParsableEnum(Enum):
         try:
             return cls[value.upper().replace("-", "_")]
         except KeyError:
-            valid_values = ', '.join(e.name.lower().replace("_", "-") for e in cls)
+            valid_values = ', '.join('`' + e.name.lower().replace("_", "-") + '`' for e in cls)
             prefix = f"Invalid value for {from_where}" if from_where else "Invalid value"
-            raise MacheteException(f"{prefix}: `{value}`. Valid values are `{valid_values}`")
+            printed_value = value or '<empty>'
+            raise MacheteException(f"{prefix}: `{printed_value}`. Valid values are {valid_values}")
 
 
 class PickRoot(Enum):
@@ -73,6 +74,12 @@ class PickRoot(Enum):
     LAST = auto()
 
 
+class PRDescriptionIntroStyle(ParsableEnum):
+    FULL = auto()
+    UP_ONLY = auto()
+    NONE = auto()
+
+
 class SquashMergeDetection(ParsableEnum):
     NONE = auto()
     SIMPLE = auto()
@@ -2220,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:
@@ -2251,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
@@ -2432,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)
@@ -2592,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
 
@@ -2721,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'
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/generated_docs.py b/git_machete/generated_docs.py
index af04dc6..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.
@@ -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 010a705..29cc057 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -344,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
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 3fc9c2f..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',
             )
         )
 


Test patch:
diff --git a/tests/pytest_full_operands.py b/tests/pytest_full_operands.py
index 9a3adac..6de3672 100644
--- a/tests/pytest_full_operands.py
+++ b/tests/pytest_full_operands.py
@@ -23,8 +23,8 @@ def pytest_assertrepr_compare(config, op: str, left, right):
             return [''] + ['  ' + x for x in arg.splitlines()] + ['']
         return [
             'Comparing values:',
-            'left:', *lines_for(left),
-            'right:', *lines_for(right)
+            'left (typically meaning actual):', *lines_for(left),
+            'right (typically meaning expected):', *lines_for(right)
         ]
 
 
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 e3ef9cd..e426576 100644
--- a/tests/test_gitlab.py
+++ b/tests/test_gitlab.py
@@ -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_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 4e8a189..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),
         )
 
@@ -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:]