Repository: VirtusLab/git-machete
Pull request #1370 (2024-11-08 11:33:54)

Release v3.31.0


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4212456..510b17b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -141,9 +141,6 @@ jobs:
           root: .
           paths: [ .coverage.* ]
 
-  # Note that if docs are ever to be built under a different Python version in CI,
-  # this change needs to be reflected in tox.ini and .readthedocs.yaml as well
-  # to avoid errors like https://github.com/VirtusLab/git-machete/issues/936.
   "python 3_11 git 2_38_1":
     executor: ubuntu_executor
     steps:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 28b9ed5..a826a51 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,13 @@
 # Release notes
 
+## New in git-machete 3.31.0
+
+- added: `git machete git{hub,lab} update-{pr,mr}-descriptions` subcommands
+- added: `git machete gith{hub,lab} create-{pr,mr} --update-related-descriptions` flags
+- added: `git machete gith{hub,lab} restack-{pr,mr} --update-related-descriptions` flags
+- added: `git machete gith{hub,lab} retarget-{pr,mr} --update-related-descriptions` flags
+- added: `machete.traverse.fetch.<remote>` git config key to selectively exclude remotes from `git machete traverse --fetch` (contributed by @gjulianm)
+
 ## New in git-machete 3.30.0
 
 - added: support for Python 3.13 (earlier versions of git-machete should also work on Python 3.13 outside certain rare cases when in `--debug` mode)
diff --git a/ci/tox/build-context/requirements.txt b/ci/tox/build-context/requirements.txt
index c79554a..e2b6945 100644
--- a/ci/tox/build-context/requirements.txt
+++ b/ci/tox/build-context/requirements.txt
@@ -4,8 +4,8 @@ build==1.2.2.post1; python_version >= '3.8'
 
 setuptools==59.6.0; python_version <= '3.6'
 setuptools==68.0.0; python_version >= '3.7' and python_version < '3.8'
-setuptools==75.1.0; python_version >= '3.8'
+setuptools==75.3.0; python_version >= '3.8'
 
 tox==3.28.0; python_version <= '3.6'
 tox==4.8.0; python_version >= '3.7' and python_version < '3.8'
-tox==4.21.2; python_version >= '3.8'
+tox==4.23.2; python_version >= '3.8'
diff --git a/completion/git-machete.completion.bash b/completion/git-machete.completion.bash
index cde9a35..3f10474 100644
--- a/completion/git-machete.completion.bash
+++ b/completion/git-machete.completion.bash
@@ -6,8 +6,8 @@ _git_machete() {
 
   local categories="addable childless managed slidable slidable-after unmanaged with-overridden-fork-point"
   local directions="down first last next prev root up"
-  local github_subcommands="anno-prs checkout-prs create-pr restack-pr retarget-pr"
-  local gitlab_subcommands="anno-mrs checkout-mrs create-mr restack-mr retarget-mr"
+  local github_subcommands="anno-prs checkout-prs create-pr restack-pr retarget-pr update-pr-descriptions"
+  local gitlab_subcommands="anno-mrs checkout-mrs create-mr restack-mr retarget-mr update-mr-descriptions"
   local locations="current $directions"
   local opt_color_args="always auto never"
   local opt_return_to_args="here nearest-remaining stay"
@@ -22,14 +22,12 @@ _git_machete() {
   local diff_opts="-s --stat"
   local discover_opts="-C --checked-out-since= -l --list-commits -r --roots= -y --yes"
   local fork_point_opts="--inferred --override-to= --override-to-inferred --override-to-parent --unset-override"
-  local github_anno_prs_opts="--with-urls"
-  local github_create_pr_opts="--draft --title= --yes"
-  local github_checkout_prs_opts="--all --by= --mine"
-  local github_retarget_pr_opts="-b --branch= --ignore-if-missing"
-  local gitlab_anno_mrs_opts="--with-urls"
-  local gitlab_create_mr_opts="--draft --title= --yes"
-  local gitlab_checkout_mrs_opts="--all --by= --mine"
-  local gitlab_retarget_mr_opts="-b --branch= --ignore-if-missing"
+  local githublab_anno_opts="--with-urls"
+  local githublab_create_opts="--draft --title= -U --update-related-descriptions --yes"
+  local githublab_checkout_opts="--all --by= --mine"
+  local githublab_restack_opts="-U --update-related-descriptions"
+  local githublab_retarget_opts="-b --branch= --ignore-if-missing -U --update-related-descriptions"
+  local githublab_update_descriptions_opts="--all --mine --related"
   local reapply_opts="-f --fork-point="
   local slide_out_opts="-d --down-fork-point= --delete -M --merge -n --no-edit-merge --no-interactive-rebase --removed-from-remote"
   local squash_opts="-f --fork-point="
@@ -56,29 +54,25 @@ _git_machete() {
         discover) __gitcomp "$common_opts $discover_opts" ;;
         fork-point) __gitcomp "$common_opts $fork_point_opts" ;;
         github)
-          if [[ ${COMP_WORDS[3]} == "anno-prs" ]]; then
-            __gitcomp "$common_opts $github_anno_prs_opts"
-          elif [[ ${COMP_WORDS[3]} == "create-pr" ]]; then
-            __gitcomp "$common_opts $github_create_pr_opts"
-          elif [[ ${COMP_WORDS[3]} == "checkout-prs" ]]; then
-            __gitcomp "$common_opts $github_checkout_prs_opts"
-          elif [[ ${COMP_WORDS[3]} == "retarget-pr" ]]; then
-            __gitcomp "$common_opts $github_retarget_pr_opts"
-          else
-            __gitcomp "$common_opts"
-          fi ;;
+          case ${COMP_WORDS[3]} in
+            "anno-prs") __gitcomp "$common_opts $githublab_anno_opts" ;;
+            "create-pr") __gitcomp "$common_opts $githublab_create_opts" ;;
+            "checkout-prs") __gitcomp "$common_opts $githublab_checkout_opts" ;;
+            "restack-pr") __gitcomp "$common_opts $githublab_restack_opts" ;;
+            "retarget-pr") __gitcomp "$common_opts $githublab_retarget_opts" ;;
+            "update-pr-descriptions") __gitcomp "$common_opts $githublab_update_descriptions_opts" ;;
+            *) __gitcomp "$common_opts" ;;
+          esac ;;
         gitlab)
-          if [[ ${COMP_WORDS[3]} == "anno-mrs" ]]; then
-            __gitcomp "$common_opts $gitlab_anno_mrs_opts"
-          elif [[ ${COMP_WORDS[3]} == "create-mr" ]]; then
-            __gitcomp "$common_opts $gitlab_create_mr_opts"
-          elif [[ ${COMP_WORDS[3]} == "checkout-mrs" ]]; then
-            __gitcomp "$common_opts $gitlab_checkout_mrs_opts"
-          elif [[ ${COMP_WORDS[3]} == "retarget-mr" ]]; then
-            __gitcomp "$common_opts $gitlab_retarget_mr_opts"
-          else
-            __gitcomp "$common_opts"
-          fi ;;
+          case ${COMP_WORDS[3]} in
+            "anno-mrs") __gitcomp "$common_opts $githublab_anno_opts" ;;
+            "create-mr") __gitcomp "$common_opts $githublab_create_opts" ;;
+            "checkout-mrs") __gitcomp "$common_opts $githublab_checkout_opts" ;;
+            "restack-mr") __gitcomp "$common_opts $githublab_restack_opts" ;;
+            "retarget-mr") __gitcomp "$common_opts $githublab_retarget_opts" ;;
+            "update-mr-descriptions") __gitcomp "$common_opts $githublab_update_descriptions_opts" ;;
+            *) __gitcomp "$common_opts" ;;
+          esac ;;
         reapply) __gitcomp "$common_opts $reapply_opts" ;;
         slide-out) __gitcomp "$common_opts $slide_out_opts" ;;
         squash) __gitcomp "$common_opts $squash_opts" ;;
@@ -132,30 +126,30 @@ _git_machete() {
               github)
                 if [[ $COMP_CWORD -eq 3 ]]; then
                   __gitcomp "$github_subcommands"
-                elif [[ ${COMP_WORDS[3]} == "anno-prs" ]]; then
-                  __gitcomp "$common_opts $github_anno_prs_opts"
-                elif [[ ${COMP_WORDS[3]} == "create-pr" ]]; then
-                  __gitcomp "$common_opts $github_create_pr_opts"
-                elif [[ ${COMP_WORDS[3]} == "checkout-prs" ]]; then
-                  __gitcomp "$common_opts $github_checkout_prs_opts"
-                elif [[ ${COMP_WORDS[3]} == "retarget-pr" ]]; then
-                  __gitcomp "$common_opts $github_retarget_pr_opts"
                 else
-                  COMPREPLY=('')
+                  case ${COMP_WORDS[3]} in
+                    "anno-prs") __gitcomp "$common_opts $githublab_anno_opts" ;;
+                    "create-pr") __gitcomp "$common_opts $githublab_create_opts" ;;
+                    "checkout-prs") __gitcomp "$common_opts $githublab_checkout_opts" ;;
+                    "restack-pr") __gitcomp "$common_opts $githublab_restack_opts" ;;
+                    "retarget-pr") __gitcomp "$common_opts $githublab_retarget_opts" ;;
+                    "update-pr-descriptions") __gitcomp "$common_opts $githublab_update_descriptions_opts" ;;
+                    *) COMPREPLY=('') ;;
+                  esac
                 fi ;;
               gitlab)
                 if [[ $COMP_CWORD -eq 3 ]]; then
                   __gitcomp "$gitlab_subcommands"
-                elif [[ ${COMP_WORDS[3]} == "anno-mrs" ]]; then
-                  __gitcomp "$common_opts $gitlab_anno_mrs_opts"
-                elif [[ ${COMP_WORDS[3]} == "create-mr" ]]; then
-                  __gitcomp "$common_opts $gitlab_create_mr_opts"
-                elif [[ ${COMP_WORDS[3]} == "checkout-mrs" ]]; then
-                  __gitcomp "$common_opts $gitlab_checkout_mrs_opts"
-                elif [[ ${COMP_WORDS[3]} == "retarget-mr" ]]; then
-                  __gitcomp "$common_opts $gitlab_retarget_mr_opts"
                 else
-                  COMPREPLY=('')
+                  case ${COMP_WORDS[3]} in
+                    "anno-mrs") __gitcomp "$common_opts $githublab_anno_opts" ;;
+                    "create-mr") __gitcomp "$common_opts $githublab_create_opts" ;;
+                    "checkout-mrs") __gitcomp "$common_opts $githublab_checkout_opts" ;;
+                    "restack-mr") __gitcomp "$common_opts $githublab_restack_opts" ;;
+                    "retarget-mr") __gitcomp "$common_opts $githublab_retarget_opts" ;;
+                    "update-mr-descriptions") __gitcomp "$common_opts $githublab_update_descriptions_opts" ;;
+                    *) COMPREPLY=('') ;;
+                  esac
                 fi ;;
               help)
                 if [[ $COMP_CWORD -eq 3 ]]; then
diff --git a/completion/git-machete.completion.zsh b/completion/git-machete.completion.zsh
index 9d960b3..99b3ceb 100644
--- a/completion/git-machete.completion.zsh
+++ b/completion/git-machete.completion.zsh
@@ -254,6 +254,7 @@ __git_machete_github_subcommands() {
         'create-pr:create a PR for the current branch, using the upstream (parent) branch as the PR base'
         'restack-pr:(force-)push and retarget the PR, without adding code owners as reviewers in the process'
         'retarget-pr:set the base of the current branch PR to upstream (parent) branch'
+        'update-pr-descriptions:update the generated sections of PR descriptions that lists the upstream and/or downstream PRs'
       )
       _describe 'subcommand' github_subcommands
       ;;
@@ -271,7 +272,7 @@ __git_machete_github_subcommands() {
           _arguments \
             '(--all)'--all'[Checkout all open PRs]' \
             '(--by)'--by='[Checkout open PRs authored by the given GitHub user]' \
-            '(--mine)'--mine='[Checkout open PRs for the current user associated with the GitHub token]' \
+            '(--mine)'--mine'[Checkout open PRs for the current user associated with the GitHub token]' \
             "${common_flags[@]}"
         ;;
 
@@ -279,14 +280,30 @@ __git_machete_github_subcommands() {
           _arguments \
             '(--draft)'--draft'[Create the new PR as draft]' \
             '(--title)'--title='[Set the title for new PR explicitly]' \
+            '(-U --update-related-descriptions)'{-U,--update-related-descriptions}'[Update the generated sections of PR descriptions that list the upstream and/or downstream PRs]' \
             '(--yes)'--yes'[Do not ask for confirmation whether to push the branch]' \
             "${common_flags[@]}"
         ;;
 
+        (restack-pr)
+          _arguments \
+            '(-U --update-related-descriptions)'{-U,--update-related-descriptions}'[Update the generated sections of PR descriptions that list the upstream and/or downstream PRs]' \
+            "${common_flags[@]}"
+        ;;
+
         (retarget-pr)
           _arguments \
             '(-b --branch)'{-b,--branch=}'[Specify the branch for which the associated PR base will be set to its upstream (parent) branch]: :__git_machete_list_managed' \
             '(--ignore-if-missing)'--ignore-if-missing'[Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch]' \
+            '(-U --update-related-descriptions)'{-U,--update-related-descriptions}'[Update the generated sections of PR descriptions that list the upstream and/or downstream PRs]' \
+            "${common_flags[@]}"
+        ;;
+
+        (update-pr-descriptions)
+          _arguments \
+            '(--all)'--all'[Update PR descriptions for all PRs in the repository]' \
+            '(--mine)'--mine'[Update PR descriptions for all PRs opened by the current user associated with the GitHub token]' \
+            '(--related)'--related'[Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch]' \
             "${common_flags[@]}"
         ;;
       esac
@@ -312,6 +329,7 @@ __git_machete_gitlab_subcommands() {
         'create-mr:create a MR for the current branch, using the upstream (parent) branch as the MR source branch'
         'restack-mr:(force-)push and retarget the MR, without adding code owners as reviewers in the process'
         'retarget-mr:set the source branch of the current branch MR to upstream (parent) branch'
+        'update-mr-descriptions:update the generated sections of MR descriptions that list the upstream and/or downstream MRs'
       )
       _describe 'subcommand' gitlab_subcommands
       ;;
@@ -329,7 +347,7 @@ __git_machete_gitlab_subcommands() {
           _arguments \
             '(--all)'--all'[Checkout all open MRs]' \
             '(--by)'--by='[Checkout open MRs authored by the given GitLab user]' \
-            '(--mine)'--mine='[Checkout open MRs for the current user associated with the GitLab token]' \
+            '(--mine)'--mine'[Checkout open MRs for the current user associated with the GitLab token]' \
             "${common_flags[@]}"
         ;;
 
@@ -337,14 +355,30 @@ __git_machete_gitlab_subcommands() {
           _arguments \
             '(--draft)'--draft'[Create the new MR as draft]' \
             '(--title)'--title='[Set the title for new MR explicitly]' \
+            '(-U --update-related-descriptions)'{-U,--update-related-descriptions}'[Update the generated sections of MR descriptions that list the upstream and/or downstream MRs]' \
             '(--yes)'--yes'[Do not ask for confirmation whether to push the branch]' \
             "${common_flags[@]}"
         ;;
 
+        (restack-mr)
+          _arguments \
+            '(-U --update-related-descriptions)'{-U,--update-related-descriptions}'[Update the generated sections of MR descriptions that list the upstream and/or downstream MRs]' \
+            "${common_flags[@]}"
+        ;;
+
         (retarget-mr)
           _arguments \
             '(-b --branch)'{-b,--branch=}'[Specify the branch for which the associated MR source branch will be set to its upstream (parent) branch]: :__git_machete_list_managed' \
             '(--ignore-if-missing)'--ignore-if-missing'[Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch]' \
+            '(-U --update-related-descriptions)'{-U,--update-related-descriptions}'[Update the generated sections of MR descriptions that list the upstream and/or downstream MRs]' \
+            "${common_flags[@]}"
+        ;;
+
+        (update-mr-descriptions)
+          _arguments \
+            '(--all)'--all'[Update MR descriptions for all MRs in the project]' \
+            '(--mine)'--mine'[Update MR descriptions for all MRs opened by the current user associated with the GitLab token]' \
+            '(--related)'--related'[Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch]' \
             "${common_flags[@]}"
         ;;
       esac
diff --git a/completion/git-machete.fish b/completion/git-machete.fish
index a568877..7cd803f 100644
--- a/completion/git-machete.fish
+++ b/completion/git-machete.fish
@@ -93,38 +93,52 @@ complete -c git-machete -n "__fish_seen_subcommand_from fork-point; and not __fi
 complete -c git-machete -n "__fish_seen_subcommand_from fork-point; and not __fish_seen_subcommand_from --inferred                  --override-to --override-to-inferred --override-to-parent" -x -l unset-override        -a '(__machete_with_overridden_fork_point_branches)'
 
 # git machete github
-complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands"                                                                                          -f -a github                                               -d 'Create, check out and manage GitHub PRs while keeping them reflected in git machete'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync"          -f -a anno-prs                                             -d 'Annotate the branches based on their corresponding GitHub PR numbers and authors'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync"          -x -a checkout-prs                                         -d 'Check out the head branch of the given pull requests (specified by number), also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync"          -f -a create-pr                                            -d 'Create a PR for the current branch, using the upstream (parent) branch as the PR base'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync"          -f -a restack-pr                                           -d '(Force-)pushes and retargets the PR, without adding code owners as reviewers in the process'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync"          -f -a retarget-pr                                          -d 'Sets the base of PR for the current branch to upstream (parent) branch, as seen by git machete (see git machete show up)'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from anno-prs;     and not __fish_seen_subcommand_from --with-urls"            -f -l with-urls                                            -d 'Include PR URLs in the annotations'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --all"                  -f -l all                                                  -d 'Checkout all open PRs'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --by"                   -x -l by                                                   -d "Checkout someone's open PRs"
-complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --mine"                 -x -l mine                                                 -d 'Checkout open PRs for the current user associated with the GitHub token'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr;    and not __fish_seen_subcommand_from --draft"                -f -l draft                                                -d 'Create the new PR as a draft'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr;    and not __fish_seen_subcommand_from --title"                -x -l title                                                -d 'Set the title for new PR explicitly'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr;    and not __fish_seen_subcommand_from --yes"                  -f -l yes                                                  -d 'Do not ask for confirmation whether to push the branch'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from retarget-pr;  and not __fish_seen_subcommand_from --branch"               -x -l branch -s b       -a '(__machete_managed_branches)'  -d 'Specify the branch for which the associated PR base will be set to its upstream (parent) branch'
-complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from retarget-pr;  and not __fish_seen_subcommand_from --ignore-if-missing"    -f -l ignore-if-missing                                    -d 'Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch'
+complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands"                                                                                                           -f -a github                                               -d 'Create, check out and manage GitHub PRs while keeping them reflected in git machete'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions"    -f -a anno-prs                                             -d 'Annotate the branches based on their corresponding GitHub PR numbers and authors'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions"    -x -a checkout-prs                                         -d 'Check out the head branch of the given pull requests (specified by number), also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions"    -f -a create-pr                                            -d 'Create a PR for the current branch, using the upstream (parent) branch as the PR base'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions"    -f -a restack-pr                                           -d '(Force-)push and retarget the PR, without adding code owners as reviewers in the process'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions"    -f -a retarget-pr                                          -d 'Set the base of PR for the current branch to upstream (parent) branch, as seen by git machete (see git machete show up)'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions"    -f -a update-pr-descriptions                               -d 'Update the generated sections of PR descriptions that lists the upstream and/or downstream PRs'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from anno-prs;               and not __fish_seen_subcommand_from --with-urls"                   -f -l with-urls                                            -d 'Include PR URLs in the annotations'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs;           and not __fish_seen_subcommand_from --all"                         -f -l all                                                  -d 'Checkout all open PRs'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs;           and not __fish_seen_subcommand_from --by"                          -x -l by                                                   -d "Checkout someone's open PRs"
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs;           and not __fish_seen_subcommand_from --mine"                        -x -l mine                                                 -d 'Checkout open PRs for the current user associated with the GitHub token'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr;              and not __fish_seen_subcommand_from --draft"                       -f -l draft                                                -d 'Create the new PR as a draft'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr;              and not __fish_seen_subcommand_from --title"                       -x -l title                                                -d 'Set the title for new PR explicitly'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr;              and not __fish_seen_subcommand_from --update-related-descriptions" -f -l update-related-descriptions -s U                     -d 'Update the generated sections of PR descriptions that list the upstream and/or downstream PRs'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr;              and not __fish_seen_subcommand_from --yes"                         -f -l yes                                                  -d 'Do not ask for confirmation whether to push the branch'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from restack-pr;             and not __fish_seen_subcommand_from --update-related-descriptions" -f -l update-related-descriptions -s U                     -d 'Update the generated sections of PR descriptions that list the upstream and/or downstream PRs'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from retarget-pr;            and not __fish_seen_subcommand_from --branch"                      -x -l branch -s b       -a '(__machete_managed_branches)'  -d 'Specify the branch for which the associated PR base will be set to its upstream (parent) branch'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from retarget-pr;            and not __fish_seen_subcommand_from --ignore-if-missing"           -f -l ignore-if-missing                                    -d 'Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from retarget-pr;            and not __fish_seen_subcommand_from --update-related-descriptions" -f -l update-related-descriptions -s U                     -d 'Update the generated sections of PR descriptions that list the upstream and/or downstream PRs'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from update-pr-descriptions; and not __fish_seen_subcommand_from --all"                         -f -l all                                                  -d 'Update PR descriptions for all PRs in the repository'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from update-pr-descriptions; and not __fish_seen_subcommand_from --mine"                        -x -l mine                                                 -d 'Update PR descriptions for all PRs opened by the current user associated with the GitHub token'
+complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from update-pr-descriptions; and not __fish_seen_subcommand_from --related"                     -x -l related                                              -d 'Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch'
 
 # git machete gitlab
-complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands"                                                                                          -f -a gitlab                                               -d 'Create, check out and manage GitLab MRs while keeping them reflected in git machete'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync"          -f -a anno-mrs                                             -d 'Annotate the branches based on their corresponding GitLab MR numbers and authors'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync"          -x -a checkout-mrs                                         -d 'Check out the head branch of the given merge requests (specified by number), also traverse chain of merge requests upwards, adding branches one by one to git-machete and check them out locally'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync"          -f -a create-mr                                            -d 'Create a MR for the current branch, using the upstream (parent) branch as the MR source branch'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync"          -f -a restack-mr                                           -d '(Force-)pushes and retargets the MR, without adding code owners as reviewers in the process'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync"          -f -a retarget-mr                                          -d 'Sets the base of MR for the current branch to upstream (parent) branch, as seen by git machete (see git machete show up)'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from anno-mrs;     and not __fish_seen_subcommand_from --with-urls"            -f -l with-urls                                            -d 'Include MR URLs in the annotations'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs; and not __fish_seen_subcommand_from --all"                  -f -l all                                                  -d 'Checkout all open MRs'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs; and not __fish_seen_subcommand_from --by"                   -x -l by                                                   -d "Checkout someone's open MRs"
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs; and not __fish_seen_subcommand_from --mine"                 -x -l mine                                                 -d 'Checkout open MRs for the current user associated with the GitLab token'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr;    and not __fish_seen_subcommand_from --draft"                -f -l draft                                                -d 'Create the new MR as a draft'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr;    and not __fish_seen_subcommand_from --title"                -x -l title                                                -d 'Set the title for new MR explicitly'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr;    and not __fish_seen_subcommand_from --yes"                  -f -l yes                                                  -d 'Do not ask for confirmation whether to push the branch'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from retarget-mr;  and not __fish_seen_subcommand_from --branch"               -x -l branch -s b       -a '(__machete_managed_branches)'  -d 'Specify the branch for which the associated MR source branch will be set to its upstream (parent) branch'
-complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from retarget-mr;  and not __fish_seen_subcommand_from --ignore-if-missing"    -f -l ignore-if-missing                                    -d 'Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch'
+complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands"                                                                                                           -f -a gitlab                                               -d 'Create, check out and manage GitLab MRs while keeping them reflected in git machete'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions"    -f -a anno-mrs                                             -d 'Annotate the branches based on their corresponding GitLab MR numbers and authors'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions"    -x -a checkout-mrs                                         -d 'Check out the head branch of the given merge requests (specified by number), also traverse chain of merge requests upwards, adding branches one by one to git-machete and check them out locally'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions"    -f -a create-mr                                            -d 'Create a MR for the current branch, using the upstream (parent) branch as the MR source branch'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions"    -f -a restack-mr                                           -d '(Force-)push and retarget the MR, without adding code owners as reviewers in the process'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions"    -f -a retarget-mr                                          -d 'Set the base of MR for the current branch to upstream (parent) branch, as seen by git machete (see git machete show up)'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions"    -f -a update-mr-descriptions                               -d 'Update the generated sections of MR descriptions that list the upstream and/or downstream MRs'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from anno-mrs;               and not __fish_seen_subcommand_from --with-urls"                   -f -l with-urls                                            -d 'Include MR URLs in the annotations'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs;           and not __fish_seen_subcommand_from --all"                         -f -l all                                                  -d 'Checkout all open MRs'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs;           and not __fish_seen_subcommand_from --by"                          -x -l by                                                   -d "Checkout someone's open MRs"
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs;           and not __fish_seen_subcommand_from --mine"                        -x -l mine                                                 -d 'Checkout open MRs for the current user associated with the GitLab token'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr;              and not __fish_seen_subcommand_from --draft"                       -f -l draft                                                -d 'Create the new MR as a draft'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr;              and not __fish_seen_subcommand_from --title"                       -x -l title                                                -d 'Set the title for new MR explicitly'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr;              and not __fish_seen_subcommand_from --update-related-descriptions" -f -l update-related-descriptions -s U                     -d 'Update the generated sections of MR descriptions that list the upstream and/or downstream MRs'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr;              and not __fish_seen_subcommand_from --yes"                         -f -l yes                                                  -d 'Do not ask for confirmation whether to push the branch'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from restack-mr;             and not __fish_seen_subcommand_from --update-related-descriptions" -f -l update-related-descriptions -s U                     -d 'Update the generated sections of MR descriptions that list the upstream and/or downstream MRs'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from retarget-mr;            and not __fish_seen_subcommand_from --branch"                      -x -l branch -s b       -a '(__machete_managed_branches)'  -d 'Specify the branch for which the associated MR source branch will be set to its upstream (parent) branch'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from retarget-mr;            and not __fish_seen_subcommand_from --ignore-if-missing"           -f -l ignore-if-missing                                    -d 'Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from retarget-mr;            and not __fish_seen_subcommand_from --update-related-descriptions" -f -l update-related-descriptions -s U                     -d 'Update the generated sections of MR descriptions that list the upstream and/or downstream MRs'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from update-mr-descriptions; and not __fish_seen_subcommand_from --all"                         -f -l all                                                  -d 'Update MR descriptions for all MRs in the project'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from update-mr-descriptions; and not __fish_seen_subcommand_from --mine"                        -x -l mine                                                 -d 'Update MR descriptions for all MRs opened by the current user associated with the GitHub token'
+complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from update-mr-descriptions; and not __fish_seen_subcommand_from --related"                     -x -l related                                              -d 'Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch'
 
 # git machete go
 complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands"                                                                   -f -a go    -d 'Check out the branch relative to the position of the current branch, accepts down/first/last/next/root/prev/up argument'
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 59c3185..10873f1 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" "Oct 09, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Nov 07, 2024" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.30.0
+git-machete \- git-machete 3.31.0
 .sp
 git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
 .sp
@@ -483,6 +483,12 @@ develop
 .UNINDENT
 .UNINDENT
 .TP
+.B \fBmachete.traverse.fetch.<remote>\fP:
+Configures the behavior of \fBgit machete traverse\fP command for the given remote when \fB\-\-fetch\fP flag is used.
+If set to \fBfalse\fP, this remote will not be fetched before the traversal.
+The default value of this configuration is \fBtrue\fP\&.
+This is useful for excluding remotes that are temporarily offline, or take a long time to respond.
+.TP
 .B \fBmachete.traverse.push\fP:
 To change the behavior of \fBgit machete traverse\fP command so that it doesn\(aqt push branches by default,
 you need to set config key \fBgit config machete.traverse.push false\fP\&.
@@ -899,7 +905,7 @@ git machete github <subcommand>
 .UNINDENT
 .UNINDENT
 .sp
-where \fB<subcommand>\fP is one of: \fBanno\-prs\fP, \fBcheckout\-prs\fP, \fBcreate\-pr\fP, \fBretarget\-pr\fP or \fBrestack\-pr\fP\&.
+where \fB<subcommand>\fP is one of: \fBanno\-prs\fP, \fBcheckout\-prs\fP, \fBcreate\-pr\fP, \fBretarget\-pr\fP, \fBrestack\-pr\fP or \fBupdate\-pr\-descriptions\fP\&.
 .sp
 Creates, checks out and manages GitHub PRs while keeping them reflected in branch layout file.
 .sp
@@ -991,14 +997,14 @@ Checkout open PRs for the current user associated with the GitHub token.
 .sp
 \fB<PR\-number\-1> ... <PR\-number\-N>\fP    Pull request numbers to checkout.
 .TP
-.B \fBcreate\-pr [\-\-draft] [\-\-title=<title>] [\-\-yes]\fP:
+.B \fBcreate\-pr [\-\-draft] [\-\-title=<title>] [\-U|\-\-update\-related\-descriptions] [\-\-yes]\fP:
 Creates a PR for the current branch, using the upstream (parent) branch as the PR base.
 Once the PR is successfully created, annotates the current branch with the new PR\(aqs number.
 .sp
 If \fB\&.git/info/milestone\fP file is present, its contents (a single number \-\-\- milestone id) are used as milestone.
 If \fB\&.git/info/reviewers\fP file is present, its contents (one GitHub login per line) are used to set reviewers.
 .sp
-The subject of the first unique commit of the branch is used as PR title.
+Unless \fB\-\-title\fP is provided, the subject of the first unique commit of the branch is used as PR title.
 If \fB\&.git/info/description\fP or \fB\&.github/pull_request_template.md\fP template is present, its contents are used as PR description.
 Otherwise (or if \fBmachete.github.forceDescriptionFromCommitMessage\fP is set), PR description is taken from message body of the first unique commit of the branch.
 .sp
@@ -1016,11 +1022,15 @@ Create the new PR as a draft.
 .BI \-\-title\fB= <title>
 Set the PR title explicitly (the default is to use the first included commit\(aqs message as the title).
 .TP
+.B  \-U\fP,\fB  \-\-update\-related\-descriptions
+Update the generated sections (\(dqintros\(dq) of PR descriptions that list the upstream and/or downstream PRs.
+See help for \fBgit machete github update\-pr\-descriptions \-\-related\fP for details.
+.TP
 .B  \-\-yes
 Do not ask for confirmation whether to push the branch.
 .UNINDENT
 .TP
-.B \fBrestack\-pr\fP:
+.B \fBrestack\-pr [\-U|\-\-update\-related\-descriptions]\fP:
 Perform the following sequence of actions:
 .INDENT 7.0
 .IP 1. 3
@@ -1035,8 +1045,16 @@ If the PR has been converted to draft in step 1, it\(aqs reverted to ready for r
 .sp
 The drafting/undrafting is useful in case the GitHub repository has set up \X'tty: link https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners'\fI\%CODEOWNERS\fP\X'tty: link'\&.
 Draft PRs don\(aqt get code owners automatically added as reviewers.
+.sp
+\fBOptions:\fP
+.INDENT 7.0
 .TP
-.B \fBretarget\-pr [\-b|\-\-branch=<branch>] [\-\-ignore\-if\-missing]\fP:
+.B  \-U\fP,\fB  \-\-update\-related\-descriptions
+Update the generated sections (\(dqintros\(dq) of PR descriptions that list the upstream and/or downstream PRs.
+See help for \fBgit machete github update\-pr\-descriptions \-\-related\fP for details.
+.UNINDENT
+.TP
+.B \fBretarget\-pr [\-b|\-\-branch=<branch>] [\-\-ignore\-if\-missing] [\-U|\-\-update\-related\-descriptions]\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 include
@@ -1052,6 +1070,10 @@ Specify the branch for which the associated PR base will be set to its upstream
 .TP
 .B  \-\-ignore\-if\-missing
 Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch.
+.TP
+.B  \-U\fP,\fB  \-\-update\-related\-descriptions
+Update the generated sections (\(dqintros\(dq) of PR descriptions that list the upstream and/or downstream PRs.
+See help for \fBgit machete github update\-pr\-descriptions \-\-related\fP for details.
 .UNINDENT
 .TP
 .B \fBsync\fP:
@@ -1067,6 +1089,25 @@ deletes unmanaged branches,
 .IP 3. 3
 deletes untracked managed branches that have no downstream branch.
 .UNINDENT
+.TP
+.B \fBupdate\-pr\-descriptions\fP:
+Updates the generated sections (\(dqintros\(dq) of PR descriptions that list the upstream and/or downstream PRs
+(depending on \fBmachete.github.prDescriptionIntroStyle\fP git config key).
+.sp
+\fBOptions:\fP
+.INDENT 7.0
+.TP
+.B  \-\-all
+Update PR descriptions for all PRs in the repository.
+.TP
+.B  \-\-mine
+Update PR descriptions for all PRs opened by the current user associated with the GitHub token.
+.TP
+.B  \-\-related
+Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch.
+If \fBmachete.github.prDescriptionIntroStyle\fP is \fBup\-only\fP (default), then only downstream PR descriptions are updated.
+If \fBmachete.github.prDescriptionIntroStyle\fP is \fBfull\fP, then both downstream and upstream PR descriptions are updated.
+.UNINDENT
 .UNINDENT
 .sp
 \fBGit config keys:\fP
@@ -1139,7 +1180,7 @@ git machete gitlab <subcommand>
 .UNINDENT
 .UNINDENT
 .sp
-where \fB<subcommand>\fP is one of: \fBanno\-mrs\fP, \fBcheckout\-mrs\fP, \fBcreate\-mr\fP, \fBretarget\-mr\fP or \fBrestack\-mr\fP\&.
+where \fB<subcommand>\fP is one of: \fBanno\-mrs\fP, \fBcheckout\-mrs\fP, \fBcreate\-mr\fP, \fBretarget\-mr\fP, \fBrestack\-mr\fP or \fBupdate\-mr\-descriptions\fP\&.
 .sp
 Creates, checks out and manages GitLab MRs while keeping them reflected in branch layout file.
 .sp
@@ -1229,7 +1270,7 @@ Checkout open MRs for the current user associated with the GitLab token.
 .sp
 \fB<MR\-number\-1> ... <MR\-number\-N>\fP    Merge request numbers to checkout.
 .TP
-.B \fBcreate\-mr [\-\-draft] [\-\-title=<title>] [\-\-yes]\fP:
+.B \fBcreate\-mr [\-\-draft] [\-\-title=<title>] [\-U|\-\-update\-related\-descriptions] [\-\-yes]\fP:
 Creates a MR for the current branch, using the upstream (parent) branch as the MR source branch.
 Once the MR is successfully created, annotates the current branch with the new MR\(aqs number.
 .sp
@@ -1238,7 +1279,7 @@ Note that you need to use a global (not per\-project) milestone id. Look for som
 .sp
 If \fB\&.git/info/reviewers\fP file is present, its contents (one GitLab login per line) are used to set reviewers.
 .sp
-The subject of the first unique commit of the branch is used as MR title.
+Unless \fB\-\-title\fP is provided, the subject of the first unique commit of the branch is used as MR title.
 If \fB\&.git/info/description\fP or \fB\&.gitlab/merge_request_templates/Default.md\fP template is present, its contents are used as MR description.
 Otherwise (or if \fBmachete.gitlab.forceDescriptionFromCommitMessage\fP is set), MR description is taken from message body of the first unique commit of the branch.
 .sp
@@ -1256,11 +1297,15 @@ Create the new MR as a draft.
 .BI \-\-title\fB= <title>
 Set the MR title explicitly (the default is to use the first included commit\(aqs message as the title).
 .TP
+.B  \-U\fP,\fB  \-\-update\-related\-descriptions
+Update the generated sections (\(dqintros\(dq) of MR descriptions that list the upstream and/or downstream MRs.
+See help for \fBgit machete gitlab update\-mr\-descriptions \-\-related\fP for details.
+.TP
 .B  \-\-yes
 Do not ask for confirmation whether to push the branch.
 .UNINDENT
 .TP
-.B \fBrestack\-mr\fP:
+.B \fBrestack\-mr [\-U|\-\-update\-related\-descriptions]\fP:
 Perform the following sequence of actions:
 .INDENT 7.0
 .IP 1. 3
@@ -1275,8 +1320,16 @@ If the MR has been converted to draft in step 1, it\(aqs reverted to ready for r
 .sp
 The drafting/undrafting is useful in case the GitLab project has set up \X'tty: link https://docs.gitlab.com/ee/user/project/codeowners/'\fI\%code owners\fP\X'tty: link'\&.
 Draft MRs don\(aqt get code owners automatically added as reviewers.
+.sp
+\fBOptions:\fP
+.INDENT 7.0
 .TP
-.B \fBretarget\-mr [\-b|\-\-branch=<branch>] [\-\-ignore\-if\-missing]\fP:
+.B  \-U\fP,\fB  \-\-update\-related\-descriptions
+Update the generated sections (\(dqintros\(dq) of MR descriptions that list the upstream and/or downstream MRs.
+See help for \fBgit machete gitlab update\-mr\-descriptions \-\-related\fP for details.
+.UNINDENT
+.TP
+.B \fBretarget\-mr [\-b|\-\-branch=<branch>] [\-\-ignore\-if\-missing] [\-U|\-\-update\-related\-descriptions]\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 base the MR ends up stacked atop another MR, the MR description posted to GitLab will include
@@ -1292,6 +1345,29 @@ Specify the branch for which the associated MR source branch will be set to its
 .TP
 .B  \-\-ignore\-if\-missing
 Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch.
+.TP
+.B  \-U\fP,\fB  \-\-update\-related\-descriptions
+Update the generated sections (\(dqintros\(dq) of MR descriptions that list the upstream and/or downstream MRs.
+See help for \fBgit machete gitlab update\-mr\-descriptions \-\-related\fP for details.
+.UNINDENT
+.TP
+.B \fBupdate\-mr\-descriptions\fP:
+Updates the generated sections (\(dqintros\(dq) of MR descriptions that list the upstream and/or downstream MRs
+(depending on \fBmachete.gitlab.mrDescriptionIntroStyle\fP git config key).
+.sp
+\fBOptions:\fP
+.INDENT 7.0
+.TP
+.B  \-\-all
+Update MR descriptions for all MRs in the project.
+.TP
+.B  \-\-mine
+Update MR descriptions for all MRs opened by the current user associated with the GitLab token.
+.TP
+.B  \-\-related
+Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch.
+If \fBmachete.gitlab.mrDescriptionIntroStyle\fP is \fBup\-only\fP (default), then only downstream MR descriptions are updated.
+If \fBmachete.gitlab.mrDescriptionIntroStyle\fP is \fBfull\fP, then both downstream and upstream MR descriptions are updated.
 .UNINDENT
 .UNINDENT
 .sp
@@ -2144,6 +2220,12 @@ whether a grey edge is displayed in \fBstatus\fP,
 whether \fBtraverse\fP suggests to slide out the branch.
 .UNINDENT
 .TP
+.B \fBmachete.traverse.fetch.<remote>\fP:
+Configures the behavior of \fBgit machete traverse\fP command for the given remote when \fB\-\-fetch\fP flag is used.
+If set to \fBfalse\fP, this remote will not be fetched before the traversal.
+The default value of this configuration is \fBtrue\fP\&.
+This is useful for excluding remotes that are temporarily offline, or take a long time to respond.
+.TP
 .B \fBmachete.traverse.push\fP
 To change the behavior of \fBgit machete traverse\fP command so that it doesn\(aqt push branches by default,
 you need to set config key \fBgit config machete.traverse.push false\fP\&.
diff --git a/docs/source/cli/config.rst b/docs/source/cli/config.rst
index 879f181..e29a613 100644
--- a/docs/source/cli/config.rst
+++ b/docs/source/cli/config.rst
@@ -48,6 +48,9 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
 ``machete.status.extraSpaceBeforeBranchName``:
     .. include:: git-config-keys/status_extraSpaceBeforeBranchName.rst
 
+``machete.traverse.fetch.<remote>``:
+    .. include:: git-config-keys/traverse_fetch_remote.rst
+
 ``machete.traverse.push``:
     .. include:: git-config-keys/traverse_push.rst
 
diff --git a/docs/source/cli/github.rst b/docs/source/cli/github.rst
index 5e0667b..9ee56b9 100644
--- a/docs/source/cli/github.rst
+++ b/docs/source/cli/github.rst
@@ -8,7 +8,7 @@ github
 
     git machete github <subcommand>
 
-where ``<subcommand>`` is one of: ``anno-prs``, ``checkout-prs``, ``create-pr``, ``retarget-pr`` or ``restack-pr``.
+where ``<subcommand>`` is one of: ``anno-prs``, ``checkout-prs``, ``create-pr``, ``retarget-pr``, ``restack-pr`` or ``update-pr-descriptions``.
 
 Creates, checks out and manages GitHub PRs while keeping them reflected in branch layout file.
 
@@ -76,14 +76,14 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
 
     ``<PR-number-1> ... <PR-number-N>``    Pull request numbers to checkout.
 
-``create-pr [--draft] [--title=<title>] [--yes]``:
+``create-pr [--draft] [--title=<title>] [-U|--update-related-descriptions] [--yes]``:
     Creates a PR for the current branch, using the upstream (parent) branch as the PR base.
     Once the PR is successfully created, annotates the current branch with the new PR's number.
 
     If ``.git/info/milestone`` file is present, its contents (a single number --- milestone id) are used as milestone.
     If ``.git/info/reviewers`` file is present, its contents (one GitHub login per line) are used to set reviewers.
 
-    The subject of the first unique commit of the branch is used as PR title.
+    Unless ``--title`` is provided, the subject of the first unique commit of the branch is used as PR title.
     If ``.git/info/description`` or ``.github/pull_request_template.md`` template is present, its contents are used as PR description.
     Otherwise (or if ``machete.github.forceDescriptionFromCommitMessage`` is set), PR description is taken from message body of the first unique commit of the branch.
 
@@ -94,13 +94,16 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
 
     **Options:**
 
-    --draft            Create the new PR as a draft.
+    --draft                            Create the new PR as a draft.
 
-    --title=<title>    Set the PR title explicitly (the default is to use the first included commit's message as the title).
+    --title=<title>                    Set the PR title explicitly (the default is to use the first included commit's message as the title).
 
-    --yes              Do not ask for confirmation whether to push the branch.
+    -U, --update-related-descriptions  Update the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs.
+                                       See help for ``git machete github update-pr-descriptions --related`` for details.
 
-``restack-pr``:
+    --yes                              Do not ask for confirmation whether to push the branch.
+
+``restack-pr [-U|--update-related-descriptions]``:
     Perform the following sequence of actions:
 
     #. If the PR for the current branch is ready for review, it gets converted to a draft.
@@ -111,7 +114,12 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
     The drafting/undrafting is useful in case the GitHub repository has set up `CODEOWNERS <https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners>`_.
     Draft PRs don't get code owners automatically added as reviewers.
 
-``retarget-pr [-b|--branch=<branch>] [--ignore-if-missing]``:
+    **Options:**
+
+    -U, --update-related-descriptions  Update the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs.
+                                       See help for ``git machete github update-pr-descriptions --related`` for details.
+
+``retarget-pr [-b|--branch=<branch>] [--ignore-if-missing] [-U|--update-related-descriptions]``:
     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 include
@@ -121,9 +129,12 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
 
     **Options:**
 
-    -b, --branch=<branch>     Specify the branch for which the associated PR base will be set to its upstream (parent) branch. The current branch is used if the option is absent.
+    -b, --branch=<branch>              Specify the branch for which the associated PR base will be set to its upstream (parent) branch. The current branch is used if the option is absent.
+
+    --ignore-if-missing                Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch.
 
-    --ignore-if-missing       Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch.
+    -U, --update-related-descriptions  Update the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs.
+                                       See help for ``git machete github update-pr-descriptions --related`` for details.
 
 ``sync``:
     **Deprecated.** Use ``github checkout-prs --mine``, ``delete-unmanaged`` and ``slide-out --removed-from-remote``.
@@ -135,6 +146,20 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
     #. deletes unmanaged branches,
     #. deletes untracked managed branches that have no downstream branch.
 
+``update-pr-descriptions``:
+    Updates the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs
+    (depending on ``machete.github.prDescriptionIntroStyle`` git config key).
+
+    **Options:**
+
+    --all         Update PR descriptions for all PRs in the repository.
+
+    --mine        Update PR descriptions for all PRs opened by the current user associated with the GitHub token.
+
+    --related     Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch.
+                  If ``machete.github.prDescriptionIntroStyle`` is ``up-only`` (default), then only downstream PR descriptions are updated.
+                  If ``machete.github.prDescriptionIntroStyle`` is ``full``, then both downstream and upstream PR descriptions are updated.
+
 **Git config keys:**
 
 ``machete.github.{domain,remote,organization,repository}`` (all subcommands):
diff --git a/docs/source/cli/gitlab.rst b/docs/source/cli/gitlab.rst
index a5d8bf4..d725d2c 100644
--- a/docs/source/cli/gitlab.rst
+++ b/docs/source/cli/gitlab.rst
@@ -8,7 +8,7 @@ gitlab
 
     git machete gitlab <subcommand>
 
-where ``<subcommand>`` is one of: ``anno-mrs``, ``checkout-mrs``, ``create-mr``, ``retarget-mr`` or ``restack-mr``.
+where ``<subcommand>`` is one of: ``anno-mrs``, ``checkout-mrs``, ``create-mr``, ``retarget-mr``, ``restack-mr`` or ``update-mr-descriptions``.
 
 Creates, checks out and manages GitLab MRs while keeping them reflected in branch layout file.
 
@@ -75,7 +75,7 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 
     ``<MR-number-1> ... <MR-number-N>``    Merge request numbers to checkout.
 
-``create-mr [--draft] [--title=<title>] [--yes]``:
+``create-mr [--draft] [--title=<title>] [-U|--update-related-descriptions] [--yes]``:
     Creates a MR for the current branch, using the upstream (parent) branch as the MR source branch.
     Once the MR is successfully created, annotates the current branch with the new MR's number.
 
@@ -84,7 +84,7 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 
     If ``.git/info/reviewers`` file is present, its contents (one GitLab login per line) are used to set reviewers.
 
-    The subject of the first unique commit of the branch is used as MR title.
+    Unless ``--title`` is provided, the subject of the first unique commit of the branch is used as MR title.
     If ``.git/info/description`` or ``.gitlab/merge_request_templates/Default.md`` template is present, its contents are used as MR description.
     Otherwise (or if ``machete.gitlab.forceDescriptionFromCommitMessage`` is set), MR description is taken from message body of the first unique commit of the branch.
 
@@ -95,13 +95,16 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 
     **Options:**
 
-    --draft            Create the new MR as a draft.
+    --draft                            Create the new MR as a draft.
 
-    --title=<title>    Set the MR title explicitly (the default is to use the first included commit's message as the title).
+    --title=<title>                    Set the MR title explicitly (the default is to use the first included commit's message as the title).
 
-    --yes              Do not ask for confirmation whether to push the branch.
+    -U, --update-related-descriptions  Update the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs.
+                                       See help for ``git machete gitlab update-mr-descriptions --related`` for details.
 
-``restack-mr``:
+    --yes                              Do not ask for confirmation whether to push the branch.
+
+``restack-mr [-U|--update-related-descriptions]``:
     Perform the following sequence of actions:
 
     #. If the MR for the current branch is ready for review, it gets converted to a draft.
@@ -112,7 +115,12 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
     The drafting/undrafting is useful in case the GitLab project has set up `code owners <https://docs.gitlab.com/ee/user/project/codeowners/>`_.
     Draft MRs don't get code owners automatically added as reviewers.
 
-``retarget-mr [-b|--branch=<branch>] [--ignore-if-missing]``:
+    **Options:**
+
+    -U, --update-related-descriptions  Update the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs.
+                                       See help for ``git machete gitlab update-mr-descriptions --related`` for details.
+
+``retarget-mr [-b|--branch=<branch>] [--ignore-if-missing] [-U|--update-related-descriptions]``:
     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 base the MR ends up stacked atop another MR, the MR description posted to GitLab will include
@@ -122,9 +130,26 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 
     **Options:**
 
-    -b, --branch=<branch>     Specify the branch for which the associated MR source branch will be set to its upstream (parent) branch. The current branch is used if the option is absent.
+    -b, --branch=<branch>              Specify the branch for which the associated MR source branch will be set to its upstream (parent) branch. The current branch is used if the option is absent.
+
+    --ignore-if-missing                Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch.
+
+    -U, --update-related-descriptions  Update the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs.
+                                       See help for ``git machete gitlab update-mr-descriptions --related`` for details.
+
+``update-mr-descriptions``:
+    Updates the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs
+    (depending on ``machete.gitlab.mrDescriptionIntroStyle`` git config key).
+
+    **Options:**
+
+    --all         Update MR descriptions for all MRs in the project.
+
+    --mine        Update MR descriptions for all MRs opened by the current user associated with the GitLab token.
 
-    --ignore-if-missing       Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch.
+    --related     Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch.
+                  If ``machete.gitlab.mrDescriptionIntroStyle`` is ``up-only`` (default), then only downstream MR descriptions are updated.
+                  If ``machete.gitlab.mrDescriptionIntroStyle`` is ``full``, then both downstream and upstream MR descriptions are updated.
 
 **Git config keys:**
 
diff --git a/docs/source/cli/traverse.rst b/docs/source/cli/traverse.rst
index 331f6bd..9d350ff 100644
--- a/docs/source/cli/traverse.rst
+++ b/docs/source/cli/traverse.rst
@@ -147,5 +147,8 @@ when the current user is NOT the author of the PR/MR associated with that branch
 ``machete.squashMergeDetection``:
     .. include:: git-config-keys/squashMergeDetection.rst
 
+``machete.traverse.fetch.<remote>``:
+    .. include:: git-config-keys/traverse_fetch_remote.rst
+
 ``machete.traverse.push``
     .. include:: git-config-keys/traverse_push.rst
diff --git a/docs/source/git-config-keys/traverse_fetch_remote.rst b/docs/source/git-config-keys/traverse_fetch_remote.rst
new file mode 100644
index 0000000..083a3a9
--- /dev/null
+++ b/docs/source/git-config-keys/traverse_fetch_remote.rst
@@ -0,0 +1,4 @@
+Configures the behavior of ``git machete traverse`` command for the given remote when ``--fetch`` flag is used.
+If set to ``false``, this remote will not be fetched before the traversal.
+The default value of this configuration is ``true``.
+This is useful for excluding remotes that are temporarily offline, or take a long time to respond.
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index b1fa674..7aeb245 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.30.0'
+__version__ = '3.31.0'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 940f366..4414a9e 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -240,7 +240,7 @@ def create_cli_parser() -> argparse.ArgumentParser:
     fork_point_exclusive_optional_args.add_argument('--override-to-parent', action='store_true')
     fork_point_exclusive_optional_args.add_argument('--unset-override', action='store_true')
 
-    def add_code_hosting_parser(command: str, subcommand_suffix: str, include_sync: bool) -> Any:
+    def add_code_hosting_parser(command: str, pr_or_mr: str, include_sync: bool) -> Any:
         parser = subparsers.add_parser(
             command,
             argument_default=argparse.SUPPRESS,
@@ -248,11 +248,12 @@ def create_cli_parser() -> argparse.ArgumentParser:
             add_help=False,
             parents=[common_args_parser])
         parser.add_argument('subcommand', choices=[
-            f'anno-{subcommand_suffix}s',
-            f'checkout-{subcommand_suffix}s',
-            f'create-{subcommand_suffix}',
-            f'restack-{subcommand_suffix}',
-            f'retarget-{subcommand_suffix}'
+            f'anno-{pr_or_mr}s',
+            f'checkout-{pr_or_mr}s',
+            f'create-{pr_or_mr}',
+            f'restack-{pr_or_mr}',
+            f'retarget-{pr_or_mr}',
+            f'update-{pr_or_mr}-descriptions'
         ] + (['sync'] if include_sync else []))
         parser.add_argument('request_id', nargs='*', type=int)
         parser.add_argument('-b', '--branch')
@@ -261,9 +262,12 @@ def create_cli_parser() -> argparse.ArgumentParser:
         parser.add_argument('--draft', action='store_true')
         parser.add_argument('--ignore-if-missing', action='store_true')
         parser.add_argument('--mine', action='store_true')
+        parser.add_argument('--related', action='store_true')
         parser.add_argument('--title')
+        parser.add_argument('-U', '--update-related-descriptions', action='store_true')
         parser.add_argument('--with-urls', action='store_true')
         parser.add_argument('--yes', action='store_true')
+
     add_code_hosting_parser('github', 'pr', include_sync=True)
     add_code_hosting_parser('gitlab', 'mr', include_sync=False)
 
@@ -430,12 +434,16 @@ def update_cli_options_using_parsed_args(
 
     for opt, arg in vars(parsed_args).items():
         # --color, --debug and --verbose are handled outside this method
-        if opt == "as_first_child":
+        if opt == "all":
+            cli_opts.opt_all = True
+        elif opt == "as_first_child":
             cli_opts.opt_as_first_child = True
         elif opt == "as_root":
             cli_opts.opt_as_root = True
         elif opt == "branch":
             cli_opts.opt_branch = AnyBranchName.of(arg) if arg else None
+        elif opt == "by":
+            cli_opts.opt_by = arg
         elif opt == "checked_out_since":
             cli_opts.opt_checked_out_since = arg
         elif opt == "delete":
@@ -448,6 +456,8 @@ def update_cli_options_using_parsed_args(
             cli_opts.opt_fetch = True
         elif opt == "fork_point":
             cli_opts.opt_fork_point = AnyRevision.of(arg) if arg else None
+        elif opt == "ignore_if_missing":
+            cli_opts.opt_ignore_if_missing = True
         elif opt == "inferred":
             cli_opts.opt_inferred = True
         elif opt == "list_commits":
@@ -456,14 +466,13 @@ def update_cli_options_using_parsed_args(
             cli_opts.opt_list_commits = cli_opts.opt_list_commits_with_hashes = True
         elif opt == "merge":
             cli_opts.opt_merge = True
+        elif opt == "mine":
+            cli_opts.opt_mine = True
         elif opt == "n":
             cli_opts.opt_n = True
         elif opt == "no_detect_squash_merges":
             warn("`--no-detect-squash-merges` is deprecated, use `--squash-merge-detection=none` instead", end="\n\n")
             cli_opts.opt_squash_merge_detection_string = "none"
-        elif opt == "squash_merge_detection" and arg is not None:  # if no arg is passed, argparse will fail anyway
-            cli_opts.opt_squash_merge_detection_string = arg
-            cli_opts.opt_squash_merge_detection_origin = "`--squash-merge-detection` flag"
         elif opt == "no_edit_merge":
             cli_opts.opt_no_edit_merge = True
         elif opt == "no_interactive_rebase":
@@ -486,12 +495,17 @@ def update_cli_options_using_parsed_args(
             cli_opts.opt_push_untracked = True
         elif opt == "push_untracked":
             cli_opts.opt_push_untracked = True
+        elif opt == "related":
+            cli_opts.opt_related = True
         elif opt == "removed_from_remote":
             cli_opts.opt_removed_from_remote = True
         elif opt == "return_to":
             cli_opts.opt_return_to = arg
         elif opt == "roots":
             cli_opts.opt_roots = list(map(LocalBranchShortName.of, filter(None, arg.split(","))))
+        elif opt == "squash_merge_detection" and arg is not None:  # if no arg is passed, argparse will fail anyway
+            cli_opts.opt_squash_merge_detection_string = arg
+            cli_opts.opt_squash_merge_detection_origin = "`--squash-merge-detection` flag"
         elif opt == "start_from":
             cli_opts.opt_start_from = arg
         elif opt == "stat":
@@ -504,6 +518,8 @@ def update_cli_options_using_parsed_args(
             cli_opts.opt_title = arg
         elif opt == "unset_override":
             cli_opts.opt_unset_override = True
+        elif opt == "update_related_descriptions":
+            cli_opts.opt_update_related_descriptions = True
         elif opt == "W":
             cli_opts.opt_fetch = True
             cli_opts.opt_start_from = "first-root"
@@ -727,61 +743,84 @@ def launch(orig_args: List[str]) -> None:
                 git.checkout(dest)
         elif cmd in ("github", "gitlab"):
             subcommand = parsed_cli.subcommand
-            config = GitHubClient.spec() if cmd == "github" else GitLabClient.spec()
-            subcommand_suffix = "pr" if cmd == "github" else "mr"
+            spec = GitHubClient.spec() if cmd == "github" else GitLabClient.spec()
+            pr_or_mr = spec.pr_short_name.lower()
 
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
 
-            if 'request_id' in parsed_cli and subcommand != f'checkout-{subcommand_suffix}s':
-                raise MacheteException(f"`request_id` option is only valid with `checkout-{subcommand_suffix}s` subcommand.")
-            for command in ('all', 'by', 'mine'):
-                if command in parsed_cli and subcommand != f"checkout-{subcommand_suffix}s":
-                    raise MacheteException(f"`--{command}` option is only valid with `checkout-{subcommand_suffix}s` subcommand.")
-            if "branch" in parsed_cli and subcommand != f"retarget-{subcommand_suffix}":
-                raise MacheteException(f"`--branch` option is only valid with `retarget-{subcommand_suffix}` subcommand.")
-            if "draft" in parsed_cli and subcommand != f"create-{subcommand_suffix}":
-                raise MacheteException(f"`--draft` option is only valid with `create-{subcommand_suffix}` subcommand.")
-            if "ignore_if_missing" in parsed_cli and subcommand != f"retarget-{subcommand_suffix}":
-                raise MacheteException(f"`--ignore-if-missing` option is only valid with `retarget-{subcommand_suffix}` subcommand.")
-            if "title" in parsed_cli and subcommand != f"create-{subcommand_suffix}":
-                raise MacheteException(f"`--title` option is only valid with `create-{subcommand_suffix}` subcommand.")
-            if "with_urls" in parsed_cli and subcommand != f"anno-{subcommand_suffix}s":
-                raise MacheteException(f"`--with-urls` option is only valid with `anno-{subcommand_suffix}s` subcommand.")
-            if "yes" in parsed_cli and subcommand != f"create-{subcommand_suffix}":
-                raise MacheteException(f"`--yes` option is only valid with `create-{subcommand_suffix}` subcommand.")
-
-            if subcommand == f"anno-{subcommand_suffix}s":
-                machete_client.sync_annotations_to_prs(config, include_urls=cli_opts.opt_with_urls)
-            elif subcommand == f"checkout-{subcommand_suffix}s":
+            if "request_id" in parsed_cli and subcommand != f"checkout-{pr_or_mr}s":
+                raise MacheteException(f"{spec.pr_short_name} number is only valid with `checkout-{pr_or_mr}s` subcommand.")
+            for name, value in (("all", cli_opts.opt_all), ("mine", cli_opts.opt_mine)):
+                if value and subcommand not in (f"checkout-{pr_or_mr}s", f"update-{pr_or_mr}-descriptions"):
+                    raise MacheteException(f"`--{name}` option is only valid with "
+                                           f"`checkout-{pr_or_mr}s` and `update-{pr_or_mr}-descriptions` subcommands.")
+            if cli_opts.opt_branch is not None and subcommand != f"retarget-{pr_or_mr}":
+                raise MacheteException(f"`--branch` option is only valid with `retarget-{pr_or_mr}` subcommand.")
+            if cli_opts.opt_by is not None and subcommand != f"checkout-{pr_or_mr}s":
+                raise MacheteException(f"`--by` option is only valid with `checkout-{pr_or_mr}s` subcommand.")
+            if cli_opts.opt_draft and subcommand != f"create-{pr_or_mr}":
+                raise MacheteException(f"`--draft` option is only valid with `create-{pr_or_mr}` subcommand.")
+            if cli_opts.opt_ignore_if_missing and subcommand != f"retarget-{pr_or_mr}":
+                raise MacheteException(f"`--ignore-if-missing` option is only valid with `retarget-{pr_or_mr}` subcommand.")
+            if cli_opts.opt_related and subcommand != f"update-{pr_or_mr}-descriptions":
+                raise MacheteException(f"`--related` option is only valid with `update-{pr_or_mr}-descriptions` subcommand.")
+            if cli_opts.opt_title is not None and subcommand != f"create-{pr_or_mr}":
+                raise MacheteException(f"`--title` option is only valid with `create-{pr_or_mr}` subcommand.")
+            if (cli_opts.opt_update_related_descriptions and
+                    subcommand not in (f"create-{pr_or_mr}", f"restack-{pr_or_mr}", f"retarget-{pr_or_mr}")):
+                raise MacheteException(f"`--update-related-descriptions` option is only valid "
+                                       f"with `create-{pr_or_mr}`, `restack-{pr_or_mr}` and `retarget-{pr_or_mr}` subcommands.")
+            if cli_opts.opt_with_urls and subcommand != f"anno-{pr_or_mr}s":
+                raise MacheteException(f"`--with-urls` option is only valid with `anno-{pr_or_mr}s` subcommand.")
+            if cli_opts.opt_yes and subcommand != f"create-{pr_or_mr}":
+                raise MacheteException(f"`--yes` option is only valid with `create-{pr_or_mr}` subcommand.")
+
+            if subcommand == f"anno-{pr_or_mr}s":
+                machete_client.sync_annotations_to_prs(spec, include_urls=cli_opts.opt_with_urls)
+            elif subcommand == f"checkout-{pr_or_mr}s":
                 if len(set(parsed_cli_as_dict.keys()).intersection({'all', 'by', 'mine', 'request_id'})) != 1:
-                    raise MacheteException(f"`checkout-{subcommand_suffix}s` subcommand must take exactly one of the following options: " +
-                                           ', '.join(['--all', '--by=...', '--mine', f'{subcommand_suffix}-number(s)']))
-                machete_client.checkout_pull_requests(config,
-                                                      pr_numbers=parsed_cli.request_id if 'request_id' in parsed_cli else [],
-                                                      all=parsed_cli.all if 'all' in parsed_cli else False,
-                                                      mine=parsed_cli.mine if 'mine' in parsed_cli else False,
-                                                      by=parsed_cli.by if 'by' in parsed_cli else None,
-                                                      fail_on_missing_current_user_for_my_opened_prs=True)
-            elif subcommand == f"create-{subcommand_suffix}":
+                    raise MacheteException(
+                        f"`checkout-{pr_or_mr}s` subcommand must take exactly one of the following options: "
+                        f'`--all`, `--by=...`, `--mine`, `{pr_or_mr}-number(s)`')
+                machete_client.checkout_pull_requests(
+                    spec,
+                    pr_numbers=parsed_cli.request_id if 'request_id' in parsed_cli else [],
+                    all=cli_opts.opt_all,
+                    mine=cli_opts.opt_mine,
+                    by=cli_opts.opt_by,
+                    fail_on_missing_current_user_for_my_open_prs=True)
+            elif subcommand == f"create-{pr_or_mr}":
                 current_branch = git.get_current_branch()
                 machete_client.create_pull_request(
-                    config,
+                    spec,
                     head=current_branch,
                     opt_draft=cli_opts.opt_draft,
                     opt_onto=cli_opts.opt_onto,
                     opt_title=cli_opts.opt_title,
+                    opt_update_related_descriptions=cli_opts.opt_update_related_descriptions,
                     opt_yes=cli_opts.opt_yes)
-            elif subcommand == f"restack-{subcommand_suffix}":
-                machete_client.restack_pull_request(config)
-            elif subcommand == f"retarget-{subcommand_suffix}":
+            elif subcommand == f"restack-{pr_or_mr}":
+                machete_client.restack_pull_request(spec, opt_update_related_descriptions=cli_opts.opt_update_related_descriptions)
+            elif subcommand == f"retarget-{pr_or_mr}":
                 branch = parsed_cli.branch if 'branch' in parsed_cli else git.get_current_branch()
                 machete_client.expect_in_managed_branches(branch)
-                ignore_if_missing = parsed_cli.ignore_if_missing if 'ignore_if_missing' in parsed_cli else False
-                machete_client.retarget_pr(config, head=branch, ignore_if_missing=ignore_if_missing)
+                machete_client.retarget_pull_request(
+                    spec,
+                    head=branch,
+                    opt_ignore_if_missing=cli_opts.opt_ignore_if_missing,
+                    opt_update_related_descriptions=cli_opts.opt_update_related_descriptions
+                )
             elif subcommand == "sync":  # GitHub only
-                machete_client.checkout_pull_requests(config, pr_numbers=[], mine=True)
+                machete_client.checkout_pull_requests(spec, pr_numbers=[], mine=True)
                 machete_client.delete_unmanaged(opt_squash_merge_detection=SquashMergeDetection.NONE, opt_yes=False)
                 machete_client.delete_untracked(opt_yes=cli_opts.opt_yes)
+            elif subcommand == f"update-{pr_or_mr}-descriptions":
+                if len(set(parsed_cli_as_dict.keys()).intersection({'all', 'related', 'mine'})) != 1:
+                    raise MacheteException(
+                        f"`update-{pr_or_mr}-descriptions` subcommand must take exactly one of the following options: "
+                        '`--all`, `--mine`, `--related`')
+                machete_client.update_pull_request_descriptions(
+                    spec, all=cli_opts.opt_all, mine=cli_opts.opt_mine, related=cli_opts.opt_related)
             else:  # an unknown subcommand is handled by argparse
                 raise UnexpectedMacheteException(f"Unknown subcommand: `{subcommand}`")
         elif cmd == "is-managed":
@@ -791,9 +830,9 @@ def launch(orig_args: List[str]) -> None:
                 sys.exit(ExitCode.MACHETE_EXCEPTION)
         elif cmd == "list":
             category = parsed_cli.category
-            if category == 'slidable-after' and 'branch' not in parsed_cli_as_dict:
+            if category == 'slidable-after' and 'branch' not in parsed_cli:
                 raise MacheteException(f"`git machete list {category}` requires an extra <branch> argument")
-            elif category != 'slidable-after' and 'branch' in parsed_cli_as_dict:
+            elif category != 'slidable-after' and 'branch' in parsed_cli:
                 raise MacheteException(f"`git machete list {category}` does not expect extra arguments")
 
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
@@ -838,7 +877,7 @@ def launch(orig_args: List[str]) -> None:
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
-            if "fork_point" in parsed_cli and cli_opts.opt_fork_point:
+            if cli_opts.opt_fork_point is not None:
                 machete_client.check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch(
                     fork_point_hash=cli_opts.opt_fork_point, branch=current_branch)
 
@@ -872,7 +911,7 @@ def launch(orig_args: List[str]) -> None:
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
             current_branch = git.get_current_branch()
-            if "fork_point" in parsed_cli and cli_opts.opt_fork_point:
+            if cli_opts.opt_fork_point is not None:
                 machete_client.check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch(
                     fork_point_hash=cli_opts.opt_fork_point, branch=current_branch)
 
@@ -925,7 +964,7 @@ def launch(orig_args: List[str]) -> None:
         elif cmd == "update":
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
-            if "fork_point" in parsed_cli and cli_opts.opt_fork_point:
+            if cli_opts.opt_fork_point is not None:
                 machete_client.check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch(
                     fork_point_hash=cli_opts.opt_fork_point, branch=git.get_current_branch())
             machete_client.update(
diff --git a/git_machete/client.py b/git_machete/client.py
index 1beb5bd..b971c95 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -28,8 +28,8 @@ from .git_operations import (HEAD, AnyBranchName, AnyRevision, BranchPair,
                              LocalBranchShortName, RemoteBranchShortName,
                              SyncToRemoteStatus)
 from .utils import (AnsiEscapeCodes, PopenResult, bold, colored, debug, dim,
-                    excluding, flat_map, fmt, get_pretty_choices, get_second,
-                    tupled, underline, warn)
+                    excluding, flat_map, fmt, get_pretty_choices,
+                    get_right_arrow, get_second, tupled, underline, warn)
 
 
 class SyncToParentStatus(Enum):
@@ -53,7 +53,6 @@ sync_to_parent_status_to_junction_ascii_only_map: Dict[SyncToParentStatus, str]
     SyncToParentStatus.OutOfSync: "x-"
 }
 
-
 E = TypeVar('E', bound='Enum')
 
 
@@ -121,6 +120,22 @@ class MacheteClient:
         self.__annotations: Dict[LocalBranchShortName, Annotation] = {}
         self.__empty_line_status: Optional[bool] = None
         self.__branch_pairs_by_hash_in_reflog: Optional[Dict[FullCommitHash, List[BranchPair]]] = None
+        self.__all_open_prs: Optional[List[PullRequest]] = None
+        self.__code_hosting_client: Optional[CodeHostingClient] = None
+
+    @property
+    def code_hosting_spec(self) -> CodeHostingSpec:
+        return self.code_hosting_client._spec
+
+    @property
+    def code_hosting_client(self) -> CodeHostingClient:
+        if self.__code_hosting_client is None:
+            raise UnexpectedMacheteException("Code hosting client has not been initialized, this is an unexpected state.")
+        return self.__code_hosting_client
+
+    @code_hosting_client.setter
+    def code_hosting_client(self, value: CodeHostingClient) -> None:
+        self.__code_hosting_client = value
 
     @property
     def branch_layout_file_path(self) -> str:
@@ -774,8 +789,9 @@ class MacheteClient:
 
         if opt_fetch:
             for rem in self.__git.get_remotes():
-                print(f"Fetching {bold(rem)}...")
-                self.__git.fetch_remote(rem)
+                if self.remote_enabled_for_traverse_fetch(rem):
+                    print(f"Fetching {bold(rem)}...")
+                    self.__git.fetch_remote(rem)
             if self.__git.get_remotes():
                 print("")
 
@@ -1702,16 +1718,19 @@ class MacheteClient:
               f"and branch reset events irrelevant for fork point/upstream inference): {reflog}")
         return result
 
+    def get_all_open_prs(self) -> List[PullRequest]:
+        if self.__all_open_prs is None:
+            spec = self.code_hosting_spec
+            print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True)
+            self.__all_open_prs = self.code_hosting_client.get_open_pull_requests()
+            print(fmt('<green><b>OK</b></green>'))
+        return self.__all_open_prs
+
     def sync_annotations_to_prs(self, spec: CodeHostingSpec, include_urls: bool) -> None:
-        domain = self.__derive_code_hosting_domain(spec)
-        org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain)
-        code_hosting_client = spec.create_client(
-            domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
-        print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True)
-        current_user: Optional[str] = code_hosting_client.get_current_user_login()
+        self.__init_code_hosting_client(spec)
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
         debug(f'Current {spec.display_name} user is ' + (bold(current_user or '<none>')))
-        all_open_prs: List[PullRequest] = code_hosting_client.get_open_pull_requests()
-        print(fmt('<green><b>OK</b></green>'))
+        all_open_prs = self.get_all_open_prs()
         self.__sync_annotations_to_branch_layout_file(spec, all_open_prs, current_user, include_urls=include_urls, verbose=True)
 
     def __pull_request_annotation(self, spec: CodeHostingSpec,
@@ -1870,6 +1889,9 @@ class MacheteClient:
                     debug(f"upstream candidate {candidate} rejected ({reject_reason_message})")
         return None
 
+    def remote_enabled_for_traverse_fetch(self, remote: str) -> bool:
+        return self.__git.get_boolean_config_attr(git_config_keys.traverse_remote_fetch(remote), True)
+
     # Also includes config that is invalid (corresponding to a non-existent/GCed commit etc.).
     def has_any_fork_point_override_config(self, branch: LocalBranchShortName) -> bool:
         return (self.__git.get_config_attr_or_none(git_config_keys.override_fork_point_to(branch)) or
@@ -2163,6 +2185,32 @@ class MacheteClient:
                 f"Fork point {bold(fork_point_hash)} is not ancestor of or the tip "
                 f"of the {bold(branch)} branch.")
 
+    def update_pull_request_descriptions(self, spec: CodeHostingSpec,
+                                         *, all: bool = False, mine: bool = False, related: bool = False) -> None:
+        if self.__code_hosting_client is None:
+            self.__init_code_hosting_client(spec)
+
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
+        if not current_user and mine:
+            msg = (f"Could not determine current user name, please check that the {spec.display_name} API token provided by one of the: "
+                   f"{spec.token_providers_message}is valid.")
+            raise MacheteException(msg)
+
+        if related:
+            head = self.__git.get_current_branch()
+            related_to = self.__get_sole_pull_request_for_head(head, ignore_if_missing=False)
+        else:
+            related_to = None
+        applicable_prs: List[PullRequest] = self.__get_applicable_pull_requests(
+            all=all, by=current_user if mine else None, related_to=related_to)
+
+        for pr in applicable_prs:
+            new_description = self.__get_updated_pull_request_description(pr)
+            if pr.description != new_description:
+                self.code_hosting_client.set_description_of_pull_request(pr.number, description=new_description)
+                pr.description = new_description
+                print(fmt(f'Description of {pr.display_text()} (<b>{pr.head} {get_right_arrow()} {pr.base}</b>) has been updated'))
+
     def checkout_pull_requests(self,
                                spec: CodeHostingSpec,
                                pr_numbers: Optional[List[int]],
@@ -2170,36 +2218,31 @@ class MacheteClient:
                                all: bool = False,
                                mine: bool = False,
                                by: Optional[str] = None,
-                               fail_on_missing_current_user_for_my_opened_prs: bool = False
+                               fail_on_missing_current_user_for_my_open_prs: bool = False
                                ) -> None:
-        domain = self.__derive_code_hosting_domain(spec)
-        org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain)
-        code_hosting_client = spec.create_client(domain=domain, organization=org_repo_remote.organization,
-                                                 repository=org_repo_remote.repository)
+        domain, org_repo_remote = self.__init_code_hosting_client(spec)
 
-        current_user: Optional[str] = code_hosting_client.get_current_user_login()
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
         if not current_user and mine:
             msg = (f"Could not determine current user name, please check that the {spec.display_name} API token provided by one of the: "
                    f"{spec.token_providers_message}is valid.")
-            if fail_on_missing_current_user_for_my_opened_prs:
+            if fail_on_missing_current_user_for_my_open_prs:
                 raise MacheteException(msg)
             else:
                 warn(msg)
                 return
-        print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True)
-        all_open_prs: List[PullRequest] = code_hosting_client.get_open_pull_requests()
-        print(fmt('<green><b>OK</b></green>'))
+        if mine:
+            by = current_user
 
         applicable_prs: List[PullRequest] = self.__get_applicable_pull_requests(
-            pr_numbers, all_opened_prs=all_open_prs, code_hosting_client=code_hosting_client,
-            all=all, mine=mine, by=by, user=current_user)
+            pr_numbers=pr_numbers, all=all, by=by)
 
         debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}')
         self.__git.fetch_remote(org_repo_remote.remote)
 
         prs_to_annotate = set(applicable_prs)
         for pr in sorted(applicable_prs, key=lambda x: x.number):
-            head_org_repo_and_git_url = code_hosting_client.get_org_repo_and_git_url_by_repo_id_or_none(pr.head_repo_id)
+            head_org_repo_and_git_url = self.code_hosting_client.get_org_repo_and_git_url_by_repo_id_or_none(pr.head_repo_id)
             if head_org_repo_and_git_url:
                 head_org = head_org_repo_and_git_url.organization  # explicit access to attributes to satisfy vulture
                 head_repo = head_org_repo_and_git_url.repository
@@ -2222,14 +2265,14 @@ class MacheteClient:
             else:
                 warn(f'{pr.display_text()} comes from fork and its {spec.repository_name} is already deleted. '
                      f'No remote tracking data will be set up for {bold(pr.head)} branch.')
-                refspec = f'{code_hosting_client.get_ref_name_for_pull_request(pr.number)}:{pr.head}'
+                refspec = f'{self.code_hosting_client.get_ref_name_for_pull_request(pr.number)}:{pr.head}'
                 self.__git.fetch_refspec(org_repo_remote.remote, refspec)
                 self.__git.checkout(LocalBranchShortName.of(pr.head))
             if pr.state in ('closed', 'merged'):
                 warn(f'{pr.display_text()} is already closed.')
             debug(f'found {pr}')
 
-            pr_path: List[PullRequest] = self.__get_upwards_path_including_pr(spec, pr, all_open_prs)
+            pr_path: List[PullRequest] = self.__get_upwards_path_including_pr(pr)
             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:
@@ -2259,25 +2302,22 @@ class MacheteClient:
         if len(applicable_prs) == 1:
             self.__git.checkout(LocalBranchShortName.of(applicable_prs[0].head))
 
-    @staticmethod
-    def __get_downwards_tree_excluding_pr(original_pr: PullRequest,
-                                          all_open_prs: List[PullRequest]) -> List[Tuple[PullRequest, int]]:
+    def __get_downwards_tree_excluding_pr(self, original_pr: 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)
+            down_prs = filter(lambda x: x.base == pr.head, self.get_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]:
+    def __get_upwards_path_including_pr(self, original_pr: PullRequest) -> List[PullRequest]:
         visited_head_branches: List[str] = [original_pr.head]
         path: List[PullRequest] = [original_pr]
         pr_base: Optional[str] = original_pr.base
@@ -2288,61 +2328,60 @@ class MacheteClient:
             if pr_base in ('main', 'master'):
                 return path
             if pr_base in visited_head_branches:
+                spec = self.code_hosting_spec
                 raise MacheteException(f"There is a cycle between {spec.display_name} {spec.pr_short_name}s: " +
                                        " -> ".join(visited_head_branches + [pr_base]))
             visited_head_branches += [pr_base]
-            pr = utils.find_or_none(lambda x: x.head == pr_base, all_open_prs)
+            pr = utils.find_or_none(lambda x: x.head == pr_base, self.get_all_open_prs())
             path = (path + [pr]) if pr else path
             pr_base = pr.base if pr else None
         return path
 
-    @staticmethod
     def __get_applicable_pull_requests(
-            pr_numbers: Optional[List[int]],
-            all_opened_prs: List[PullRequest],
-            code_hosting_client: CodeHostingClient,
-            all: bool,
-            mine: bool,
-            by: Optional[str],
-            user: Optional[str]
+            self,
+            *,
+            pr_numbers: Optional[List[int]] = None,
+            all: bool = False,
+            by: Optional[str] = None,
+            related_to: Optional[PullRequest] = None
     ) -> List[PullRequest]:
         result: List[PullRequest] = []
-        spec = code_hosting_client._spec
+        spec = self.code_hosting_spec
+        all_open_prs = self.get_all_open_prs()
+        repo_pretty = f"{spec.repository_name} {bold(self.code_hosting_client.organization)}/{bold(self.code_hosting_client.repository)}"
         if pr_numbers:
             for pr_number in pr_numbers:
-                pr: Optional[PullRequest] = utils.find_or_none(lambda x: x.number == pr_number, all_opened_prs)
+                pr: Optional[PullRequest] = utils.find_or_none(lambda x: x.number == pr_number, all_open_prs)
                 if pr:
                     result.append(pr)
                 else:
-                    pr = code_hosting_client.get_pull_request_by_number_or_none(pr_number)
+                    pr = self.code_hosting_client.get_pull_request_by_number_or_none(pr_number)
                     if pr:
                         result.append(pr)
                     else:
 
-                        raise MacheteException(f"{spec.pr_short_name} {spec.pr_ordinal_char}{bold(str(pr_number))} "
-                                               f"is not found in {spec.repository_name} "
-                                               f"{bold(code_hosting_client.organization)}/{bold(code_hosting_client.repository)}")
+                        raise MacheteException(
+                            f"{spec.pr_short_name} {spec.pr_ordinal_char}{bold(str(pr_number))} is not found in {repo_pretty}")
             return result
         if all:
-            if not all_opened_prs:
-                warn(f"Currently there are no {spec.pr_full_name}s opened in {spec.repository_name} "
-                     f"{bold(code_hosting_client.organization)}/{bold(code_hosting_client.repository)}")
+            if not all_open_prs:
+                warn(f"Currently there are no {spec.pr_full_name}s opened in {repo_pretty}")
                 return []
-            return all_opened_prs
-        elif mine and user:
-            result = [pr for pr in all_opened_prs if pr.user == user]
-            if not result:
-                warn(f"Current user {bold(user)} has no open {spec.pr_full_name} in {spec.repository_name} "
-                     f"{bold(code_hosting_client.organization)}/{bold(code_hosting_client.repository)}")
-                return []
-            return result
+            return all_open_prs
         elif by:
-            result = [pr for pr in all_opened_prs if pr.user == by]
+            result = [pr for pr in all_open_prs if pr.user == by]
             if not result:
-                warn(f"User {bold(by)} has no open {spec.pr_full_name} in {spec.repository_name} "
-                     f"{bold(code_hosting_client.organization)}/{bold(code_hosting_client.repository)}")
+                warn(f"User {bold(by)} has no open {spec.pr_full_name} in {repo_pretty}")
                 return []
             return result
+        elif related_to:
+            style = self.__get_pr_description_into_style_from_config()
+            result = []
+            if style == PRDescriptionIntroStyle.FULL:
+                result += reversed(self.__get_upwards_path_including_pr(related_to))
+            result += [pr_ for pr_, _ in self.__get_downwards_tree_excluding_pr(related_to)]
+            return result
+
         raise UnexpectedMacheteException("All params passed to __get_applicable_pull_requests are empty.")
 
     def __get_url_for_remote(self) -> Dict[str, str]:
@@ -2360,22 +2399,12 @@ class MacheteClient:
                 return remote
         return None
 
-    def restack_pull_request(self, spec: CodeHostingSpec) -> None:
-        domain = self.__derive_code_hosting_domain(spec)
+    def restack_pull_request(self, spec: CodeHostingSpec, opt_update_related_descriptions: bool) -> None:
         head = self.__git.get_current_branch()
-        org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain, branch_used_for_tracking_data=head)
-        code_hosting_client = spec.create_client(
-            domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
+        _, org_repo_remote = self.__init_code_hosting_client(spec, branch_used_for_tracking_data=head)
 
-        prs: List[PullRequest] = code_hosting_client.get_open_pull_requests_by_head(head)
-        if not prs:
-            raise MacheteException(f"No {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> "
-                                   f"have <b>{head}</b> as its {spec.head_branch_name} branch")
-        if len(prs) > 1:
-            raise MacheteException(f"Multiple {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> "
-                                   f"have <b>{head}</b> as its {spec.head_branch_name} branch: " +
-                                   ", ".join(_pr.short_display_text() for _pr in prs))
-        pr = prs[0]
+        pr: Optional[PullRequest] = self.__get_sole_pull_request_for_head(head, ignore_if_missing=False)
+        assert pr is not None
 
         self.__git.fetch_remote(org_repo_remote.remote)
 
@@ -2393,12 +2422,13 @@ class MacheteClient:
                     f'Branch <b>{current_branch}</b> is marked as `push=no`; aborting the restack.\n'
                     f'Did you want to just use `git machete {spec.git_machete_command} {subcommand}`?\n')
 
-            converted_to_draft = code_hosting_client.set_draft_status_of_pull_request(pr.number, target_draft_status=True)
+            converted_to_draft = self.code_hosting_client.set_draft_status_of_pull_request(pr.number, target_draft_status=True)
             if converted_to_draft:
                 print(f'{pr.display_text()} has been temporarily marked as draft')
 
             # Note that retarget should happen BEFORE push, see issue #1222
-            self.retarget_pr(spec, head, ignore_if_missing=False)
+            self.retarget_pull_request(spec, head, opt_ignore_if_missing=False,
+                                       opt_update_related_descriptions=opt_update_related_descriptions)
 
             if s == SyncToRemoteStatus.AHEAD_OF_REMOTE:
                 assert remote is not None
@@ -2436,7 +2466,7 @@ class MacheteClient:
             self.__print_new_line(False)
 
             if converted_to_draft:
-                code_hosting_client.set_draft_status_of_pull_request(prs[0].number, target_draft_status=False)
+                self.code_hosting_client.set_draft_status_of_pull_request(pr.number, target_draft_status=False)
                 print(f'{pr.display_text()} has been marked as ready for review again')
 
         else:
@@ -2450,16 +2480,16 @@ class MacheteClient:
             else:  # case handled elsewhere
                 raise UnexpectedMacheteException(f"Could not retarget {spec.pr_full_name}: invalid sync-to-remote status `{s}`.")
 
-            self.retarget_pr(spec, head, ignore_if_missing=False)
+            self.retarget_pull_request(spec, head, opt_ignore_if_missing=False,
+                                       opt_update_related_descriptions=opt_update_related_descriptions)
 
-    def __get_updated_pull_request_description(self, code_hosting_client: CodeHostingClient,
-                                               pr: PullRequest, old_description: Optional[str]) -> str:
+    def __get_updated_pull_request_description(self, pr: PullRequest) -> str:
         def skip_leading_empty(strs: List[str]) -> List[str]:
             return list(itertools.dropwhile(lambda line: line.strip() == '', strs))
 
-        lines = skip_leading_empty(old_description.splitlines()) if old_description else []
-        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 = skip_leading_empty(pr.description.splitlines()) if pr.description else []
+        style = self.__get_pr_description_into_style_from_config()
+        text_to_prepend = self.__generate_pr_description_intro(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)
@@ -2473,29 +2503,15 @@ class MacheteClient:
             lines = lines_to_prepend + ([''] if lines_to_prepend else []) + skip_leading_empty(lines)
         return '\n'.join(lines)
 
-    def retarget_pr(self, spec: CodeHostingSpec, head: LocalBranchShortName, ignore_if_missing: bool) -> None:
-        domain = self.__derive_code_hosting_domain(spec)
-        org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain, branch_used_for_tracking_data=head)
-        code_hosting_client = spec.create_client(
-            domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
+    def retarget_pull_request(self, spec: CodeHostingSpec, head: LocalBranchShortName,
+                              opt_ignore_if_missing: bool, opt_update_related_descriptions: bool) -> None:
+        if self.__code_hosting_client is None:
+            self.__init_code_hosting_client(spec, branch_used_for_tracking_data=head)
 
-        debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}')
-
-        prs: List[PullRequest] = code_hosting_client.get_open_pull_requests_by_head(head)
-        if not prs:
-            if ignore_if_missing:
-                warn(f"no {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> "
-                     f"have <b>{head}</b> as its {spec.head_branch_name} branch")
-                return
-            else:
-                raise MacheteException(f"No {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> "
-                                       f"have <b>{head}</b> as its {spec.head_branch_name} branch")
-        if len(prs) > 1:
-            raise MacheteException(f"Multiple {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> "
-                                   f"have <b>{head}</b> as its {spec.head_branch_name} branch: " +
-                                   ", ".join(_pr.short_display_text() for _pr in prs))
-        pr = prs[0]
-        debug(f'found {pr}')
+        pr: Optional[PullRequest] = self.__get_sole_pull_request_for_head(
+            head, ignore_if_missing=opt_ignore_if_missing)
+        if pr is None:
+            return
 
         new_base: Optional[LocalBranchShortName] = self.__up_branch.get(LocalBranchShortName.of(head))
         if not new_base:
@@ -2505,26 +2521,40 @@ class MacheteClient:
                 'Consider modifying the branch layout file (`git machete edit`)'
                 f' so that {bold(head)} is a child of {bold(pr.base)}.')
 
+        pr_with_original_base = pr.copy()
         if pr.base != new_base:
-            code_hosting_client.set_base_of_pull_request(pr.number, base=new_base)
+            self.code_hosting_client.set_base_of_pull_request(pr.number, base=new_base)
             print(f'{spec.base_branch_name.capitalize()} branch of {pr.display_text()} has been switched to {bold(new_base)}')
-            pr = pr._replace(base=new_base)
+            pr.base = new_base
         else:
             print(f'{spec.base_branch_name.capitalize()} branch of {pr.display_text()} is already {bold(new_base)}')
 
-        new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, pr.description)
+        new_description = self.__get_updated_pull_request_description(pr)
         if pr.description != new_description:
-            code_hosting_client.set_description_of_pull_request(pr.number, description=new_description)
+            self.code_hosting_client.set_description_of_pull_request(pr.number, description=new_description)
             print(f'Description of {pr.display_text()} has been updated')
 
-        current_user: Optional[str] = code_hosting_client.get_current_user_login()
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
         if self.__annotations.get(head) and self.__annotations[head].qualifiers_text:
-            self.__annotations[head] = Annotation(f'{self.__pull_request_annotation(spec, pr, current_user)} ' +
+            self.__annotations[head] = Annotation(self.__pull_request_annotation(spec, pr, current_user) + ' ' +
                                                   self.__annotations[head].qualifiers_text)
         else:
             self.__annotations[head] = Annotation(self.__pull_request_annotation(spec, pr, current_user))
         self.save_branch_layout_file()
 
+        if opt_update_related_descriptions:
+            print(f"Updating descriptions of other {spec.pr_short_name}s...")
+            applicable_prs: List[PullRequest] = self.__get_applicable_pull_requests(related_to=pr_with_original_base) \
+                + self.__get_applicable_pull_requests(related_to=pr)
+            applicable_prs = [pr_ for pr_ in applicable_prs if pr_.number != pr.number]
+
+            for pr in applicable_prs:
+                new_description = self.__get_updated_pull_request_description(pr)
+                if (pr.description or '').rstrip() != new_description.rstrip():
+                    self.code_hosting_client.set_description_of_pull_request(pr.number, description=new_description)
+                    pr.description = new_description
+                    print(fmt(f'Description of {pr.display_text()} (<b>{pr.head} {get_right_arrow()} {pr.base}</b>) has been updated'))
+
     def __derive_code_hosting_domain(self, spec: CodeHostingSpec) -> str:
         return self.__git.get_config_attr_or_none(key=spec.git_config_keys.domain) or spec.default_domain
 
@@ -2617,49 +2647,87 @@ class MacheteClient:
             f'You can select the {spec.repository_name} by providing some or all of git config keys:\n'
             f'{spec.git_config_keys.for_locating_repo_message()}\n')
 
+    def __init_code_hosting_client(self, spec: CodeHostingSpec,
+                                   branch_used_for_tracking_data: Optional[LocalBranchShortName] = None
+                                   ) -> Tuple[str, OrganizationAndRepositoryAndRemote]:
+        if self.__code_hosting_client is not None:
+            raise UnexpectedMacheteException("Code hosting client has already been initialized.")
+        domain = self.__derive_code_hosting_domain(spec)
+        org_repo_remote = self.__derive_org_repo_and_remote(
+            spec, domain=domain, branch_used_for_tracking_data=branch_used_for_tracking_data)
+        self.code_hosting_client = spec.create_client(
+            domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
+        return domain, org_repo_remote
+
     START_GIT_MACHETE_GENERATED_COMMENT = '<!-- start git-machete generated -->'
     END_GIT_MACHETE_GENERATED_COMMENT = '<!-- end git-machete generated -->'
 
-    def __get_pr_description_into_style_from_config(self, spec: CodeHostingSpec) -> PRDescriptionIntroStyle:
-        config_key = spec.git_config_keys.pr_description_intro_style
+    def __get_sole_pull_request_for_head(
+            self,
+            head: LocalBranchShortName,
+            ignore_if_missing: bool
+    ) -> Optional[PullRequest]:
+        prs: List[PullRequest] = self.code_hosting_client.get_open_pull_requests_by_head(head)
+        spec = self.code_hosting_spec
+        org_and_repo = self.code_hosting_client.get_org_and_repo()
+        if not prs:
+            if ignore_if_missing:
+                warn(f"no {spec.pr_short_name}s in <b>{org_and_repo}</b> "
+                     f"have <b>{head}</b> as its {spec.head_branch_name} branch")
+                return None
+            else:
+                raise MacheteException(f"No {spec.pr_short_name}s in <b>{org_and_repo}</b> "
+                                       f"have <b>{head}</b> as its {spec.head_branch_name} branch")
+        if len(prs) > 1:
+            raise MacheteException(f"Multiple {spec.pr_short_name}s in <b>{org_and_repo}</b> "
+                                   f"have <b>{head}</b> as its {spec.head_branch_name} branch: " +
+                                   ", ".join(_pr.short_display_text() for _pr in prs))
+        pr = prs[0]
+        debug(f'found {pr}')
+        return pr
+
+    def __get_pr_description_into_style_from_config(self) -> PRDescriptionIntroStyle:
+        config_key = self.code_hosting_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"
         )
 
-    def __generate_pr_description_intro(self, code_hosting_client: CodeHostingClient,
-                                        pr: PullRequest, style: PRDescriptionIntroStyle) -> str:
+    def __generate_pr_description_intro(self, 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/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 self.__all_open_prs is not None:
+            prs_for_base_branch = list(filter(lambda _pr: _pr.head == pr.base, self.__all_open_prs))
+        else:
+            # 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/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 = self.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
+        spec = self.code_hosting_spec
         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_up_path = reversed(self.__get_upwards_path_including_pr(spec, pr, all_open_prs))
+
+        pr_up_path = list(reversed(self.__get_upwards_path_including_pr(pr)))
         if style == PRDescriptionIntroStyle.FULL:
-            pr_down_tree = self.__get_downwards_tree_excluding_pr(pr, all_open_prs)
+            pr_down_tree = self.__get_downwards_tree_excluding_pr(pr)
         else:
             pr_down_tree = []
+        if len(pr_up_path) == 1 and pr_down_tree == []:
+            return ''
 
         prepend = f'{self.START_GIT_MACHETE_GENERATED_COMMENT}\n\n'
-        # In FULL mode, we're likely to generate the intro even when there are NO upstream PRs above
+        # In FULL mode, we're likely to generate a non-empty 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:
+        if pr_down_tree and len(pr_up_path) > 1:
             prepend += f'## Chain of upstream {pr_short_name}s & tree of downstream {pr_short_name}s'
+        elif pr_down_tree:
+            prepend += f'## Tree of downstream {pr_short_name}s'
         else:
             prepend += f'## Chain of upstream {pr_short_name}s'
         current_date = utils.get_current_date()
@@ -2693,11 +2761,12 @@ class MacheteClient:
             opt_draft: bool,
             opt_onto: Optional[LocalBranchShortName],
             opt_title: Optional[str],
+            opt_update_related_descriptions: bool,
             opt_yes: bool
     ) -> None:
         # first make sure that head branch is synced with remote
         try:
-            self.__sync_before_creating_pr(spec, opt_onto=opt_onto, opt_yes=opt_yes)
+            self.__sync_before_creating_pull_request(spec, opt_onto=opt_onto, opt_yes=opt_yes)
         except InteractionStopped:
             return
 
@@ -2747,17 +2816,21 @@ class MacheteClient:
                 opt_push_untracked=True,
                 opt_yes=opt_yes)
 
-        code_hosting_client = spec.create_client(domain=domain, organization=base_org_repo_remote.organization,
-                                                 repository=base_org_repo_remote.repository)
-        current_user: Optional[str] = code_hosting_client.get_current_user_login()
+        self.code_hosting_client = spec.create_client(domain=domain, organization=base_org_repo_remote.organization,
+                                                      repository=base_org_repo_remote.repository)
+        current_user: Optional[str] = self.code_hosting_client.get_current_user_login()
         debug(f'organization is {base_org_repo_remote.organization}, repository is {base_org_repo_remote.repository}')
         debug(f'current {spec.display_name} user is ' + (current_user or '<none>'))
 
         fork_point = self.fork_point(head, use_overrides=True)
         commits: List[GitLogEntry] = self.__git.get_commits_between(fork_point, head)
 
+        pr_title_file_path = self.__git.get_main_git_subpath('info', 'title')
+        is_pr_title_file = os.path.isfile(pr_title_file_path)
         if opt_title:
             title = opt_title
+        elif is_pr_title_file:
+            title = utils.slurp_file(pr_title_file_path)
         else:
             # git-machete can still see an empty range of unique commits (e.g. in case of yellow edge)
             # even though code hosting may see a non-empty range.
@@ -2781,23 +2854,23 @@ class MacheteClient:
         ok_str = '<green><b>OK</b></green>'
         print(f'Creating a {"draft " if opt_draft else ""}{spec.pr_short_name} from {bold(head)} to {bold(base)}... ', end='', flush=True)
 
-        pr: PullRequest = code_hosting_client.create_pull_request(
+        pr: PullRequest = self.code_hosting_client.create_pull_request(
             head=head, head_org_repo=head_org_repo, base=base,
             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)
+        style = self.__get_pr_description_into_style_from_config()
         # 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.
         # 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).
-            new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, description)
+            new_description = self.__get_updated_pull_request_description(pr)
             if new_description.strip() != description.strip():
                 print(f'Updating description of {pr.display_text()} to include '
                       f'the chain of {spec.pr_short_name}s... ', end='', flush=True)
-                code_hosting_client.set_description_of_pull_request(pr.number, new_description)
+                self.code_hosting_client.set_description_of_pull_request(pr.number, new_description)
                 print(fmt(ok_str))
 
         milestone_path: str = self.__git.get_main_git_subpath('info', 'milestone')
@@ -2807,12 +2880,12 @@ class MacheteClient:
             milestone = None
         if milestone:
             print(f'Setting milestone of {pr.display_text()} to {bold(milestone)}... ', end='', flush=True)
-            code_hosting_client.set_milestone_of_pull_request(pr.number, milestone=milestone)
+            self.code_hosting_client.set_milestone_of_pull_request(pr.number, milestone=milestone)
             print(fmt(ok_str))
 
         if current_user:
             print(f'Adding {bold(current_user)} as assignee to {pr.display_text()}... ', end='', flush=True)
-            code_hosting_client.add_assignees_to_pull_request(pr.number, [current_user])
+            self.code_hosting_client.add_assignees_to_pull_request(pr.number, [current_user])
             print(fmt(ok_str))
 
         reviewers_path = self.__git.get_main_git_subpath('info', 'reviewers')
@@ -2824,12 +2897,16 @@ class MacheteClient:
             print(f'Adding {", ".join(bold(reviewer) for reviewer in reviewers)} '
                   f'as reviewer{"s" if len(reviewers) > 1 else ""} to {pr.display_text()}... ',
                   end='', flush=True)
-            code_hosting_client.add_reviewers_to_pull_request(pr.number, reviewers)
+            self.code_hosting_client.add_reviewers_to_pull_request(pr.number, reviewers)
             print(fmt(ok_str))
 
         self.__annotations[head] = Annotation(self.__pull_request_annotation(spec, pr, current_user))
         self.save_branch_layout_file()
 
+        if opt_update_related_descriptions:
+            print(f"Updating descriptions of other {spec.pr_short_name}s...")
+            self.update_pull_request_descriptions(spec, related=True)
+
     def __handle_diverged_and_newer_state(
             self,
             *,
@@ -2955,7 +3032,8 @@ class MacheteClient:
         elif ans in ('q', 'quit'):
             raise InteractionStopped
 
-    def __sync_before_creating_pr(self, spec: CodeHostingSpec, *, opt_onto: Optional[LocalBranchShortName], opt_yes: bool) -> None:
+    def __sync_before_creating_pull_request(self, spec: CodeHostingSpec, *,
+                                            opt_onto: Optional[LocalBranchShortName], opt_yes: bool) -> None:
 
         self.expect_at_least_one_managed_branch()
         self.__empty_line_status = True
diff --git a/git_machete/code_hosting.py b/git_machete/code_hosting.py
index 6c138a3..120ad82 100644
--- a/git_machete/code_hosting.py
+++ b/git_machete/code_hosting.py
@@ -7,16 +7,75 @@ from git_machete.git_operations import GitContext, LocalBranchShortName
 from git_machete.utils import bold
 
 
-class PullRequest(NamedTuple):
-    number: int
-    display_prefix: str
-    user: str
-    base: str
-    head: str
-    head_repo_id: int
-    state: str
-    description: Optional[str]
-    html_url: str
+class PullRequest:
+    def __init__(self, number: int, display_prefix: str, user: str, base: str, head: str, head_repo_id: int,
+                 state: str, description: Optional[str], html_url: str):
+        self.__number = number
+        self.__display_prefix = display_prefix
+        self.__user = user
+        self.__base = base
+        self.__head = head
+        self.__head_repo_id = head_repo_id
+        self.__state = state
+        self.__description = description
+        self.__html_url = html_url
+
+    def copy(self) -> "PullRequest":
+        return PullRequest(
+            number=self.__number,
+            display_prefix=self.__display_prefix,
+            user=self.__user,
+            base=self.__base,
+            head=self.__head,
+            head_repo_id=self.__head_repo_id,
+            state=self.__state,
+            description=self.__description,
+            html_url=self.__html_url
+        )
+
+    @property
+    def number(self) -> int:
+        return self.__number
+
+    @property
+    def display_prefix(self) -> str:
+        return self.__display_prefix
+
+    @property
+    def user(self) -> str:
+        return self.__user
+
+    @property
+    def base(self) -> str:
+        return self.__base
+
+    @base.setter
+    def base(self, base: str) -> None:
+        self.__base = base
+
+    @property
+    def head(self) -> str:
+        return self.__head
+
+    @property
+    def head_repo_id(self) -> int:
+        return self.__head_repo_id
+
+    @property
+    def state(self) -> str:
+        return self.__state
+
+    @property
+    def description(self) -> Optional[str]:
+        return self.__description
+
+    @description.setter
+    def description(self, description: str) -> None:
+        self.__description = description
+
+    @property
+    def html_url(self) -> str:
+        return self.__html_url
 
     def short_display_text(self, fmt: bool = True) -> str:
         return self.display_text(fmt).split(" ")[1]
@@ -139,6 +198,9 @@ class CodeHostingClient(metaclass=ABCMeta):  # pragma: no cover
             ctx.verify_mode = ssl.CERT_NONE
         return ctx
 
+    def get_org_and_repo(self) -> OrganizationAndRepository:
+        return OrganizationAndRepository(self.organization, self.repository)
+
     @abstractmethod
     def create_pull_request(self, head: str, head_org_repo: OrganizationAndRepository,
                             base: str, title: str, description: str, draft: bool) -> PullRequest:
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index fb4ff9d..8ae69fe 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -346,6 +346,12 @@ long_docs: Dict[str, str] = {
               │
               └─ feature_branch2
 
+           `machete.traverse.fetch.<remote>`:
+              Configures the behavior of `git machete traverse` command for the given remote when `--fetch` flag is used.
+              If set to `false`, this remote will not be fetched before the traversal.
+              The default value of this configuration is `true`.
+              This is useful for excluding remotes that are temporarily offline, or take a long time to respond.
+
            `machete.traverse.push`:
  
               To change the behavior of `git machete traverse` command so that it doesn't push branches by default,
@@ -564,7 +570,7 @@ long_docs: Dict[str, str] = {
         <b>Usage:</b><b>
            git machete github <subcommand></b>
 
-        where `<subcommand>` is one of: `anno-prs`, `checkout-prs`, `create-pr`, `retarget-pr` or `restack-pr`.
+        where `<subcommand>` is one of: `anno-prs`, `checkout-prs`, `create-pr`, `retarget-pr`, `restack-pr` or `update-pr-descriptions`.
 
         Creates, checks out and manages GitHub PRs while keeping them reflected in branch layout file.
 
@@ -630,7 +636,7 @@ long_docs: Dict[str, str] = {
 
               `<PR-number-1> ... <PR-number-N>`    Pull request numbers to checkout.
 
-           `create-pr [--draft] [--title=<title>] [--yes]`:
+           `create-pr [--draft] [--title=<title>] [-U|--update-related-descriptions] [--yes]`:
  
               Creates a PR for the current branch, using the upstream (parent) branch as the PR base.
               Once the PR is successfully created, annotates the current branch with the new PR's number.
@@ -638,7 +644,7 @@ long_docs: Dict[str, str] = {
               If `.git/info/milestone` file is present, its contents (a single number — milestone id) are used as milestone.
               If `.git/info/reviewers` file is present, its contents (one GitHub login per line) are used to set reviewers.
 
-              The subject of the first unique commit of the branch is used as PR title.
+              Unless `--title` is provided, the subject of the first unique commit of the branch is used as PR title.
               If `.git/info/description` or `.github/pull_request_template.md` template is present, its contents are used as PR description.
               Otherwise (or if `machete.github.forceDescriptionFromCommitMessage` is set), PR description is taken from message body of the first unique commit of the branch.
 
@@ -656,10 +662,15 @@ long_docs: Dict[str, str] = {
 
               Set the PR title explicitly (the default is to use the first included commit's message as the title).
 
+                 <b>-U</b>, <b>--update-related-descriptions</b>
+
+              Update the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs.
+              See help for `git machete github update-pr-descriptions --related` for details.
+
                  <b>--yes</b>
               Do not ask for confirmation whether to push the branch.
 
-           `restack-pr`:
+           `restack-pr [-U|--update-related-descriptions]`:
  
               Perform the following sequence of actions:
 
@@ -674,7 +685,14 @@ long_docs: Dict[str, str] = {
               The drafting/undrafting is useful in case the GitHub repository has set up CODEOWNERS.
               Draft PRs don't get code owners automatically added as reviewers.
 
-           `retarget-pr [-b|--branch=<branch>] [--ignore-if-missing]`:
+              <b>Options:</b>
+
+                 <b>-U</b>, <b>--update-related-descriptions</b>
+
+              Update the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs.
+              See help for `git machete github update-pr-descriptions --related` for details.
+
+           `retarget-pr [-b|--branch=<branch>] [--ignore-if-missing] [-U|--update-related-descriptions]`:
  
               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`).
 
@@ -693,6 +711,11 @@ long_docs: Dict[str, str] = {
 
               Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch.
 
+                 <b>-U</b>, <b>--update-related-descriptions</b>
+
+              Update the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs.
+              See help for `git machete github update-pr-descriptions --related` for details.
+
            `sync`:
  
               <b>Deprecated.</b> Use `github checkout-prs --mine`, `delete-unmanaged` and `slide-out --removed-from-remote`.
@@ -706,6 +729,24 @@ long_docs: Dict[str, str] = {
 
                  * deletes untracked managed branches that have no downstream branch.
 
+           `update-pr-descriptions`:
+ 
+              Updates the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs
+              (depending on `machete.github.prDescriptionIntroStyle` git config key).
+
+              <b>Options:</b>
+
+                 <b>--all</b>
+              Update PR descriptions for all PRs in the repository.
+
+                 <b>--mine</b>
+              Update PR descriptions for all PRs opened by the current user associated with the GitHub token.
+
+                 <b>--related</b>
+              Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch.
+              If `machete.github.prDescriptionIntroStyle` is `up-only` (default), then only downstream PR descriptions are updated.
+              If `machete.github.prDescriptionIntroStyle` is `full`, then both downstream and upstream PR descriptions are updated.
+
         <b>Git config keys:</b>
            `machete.github.{domain,remote,organization,repository}` (all subcommands):
  
@@ -756,7 +797,7 @@ long_docs: Dict[str, str] = {
         <b>Usage:</b><b>
            git machete gitlab <subcommand></b>
 
-        where `<subcommand>` is one of: `anno-mrs`, `checkout-mrs`, `create-mr`, `retarget-mr` or `restack-mr`.
+        where `<subcommand>` is one of: `anno-mrs`, `checkout-mrs`, `create-mr`, `retarget-mr`, `restack-mr` or `update-mr-descriptions`.
 
         Creates, checks out and manages GitLab MRs while keeping them reflected in branch layout file.
 
@@ -821,7 +862,7 @@ long_docs: Dict[str, str] = {
 
               `<MR-number-1> ... <MR-number-N>`    Merge request numbers to checkout.
 
-           `create-mr [--draft] [--title=<title>] [--yes]`:
+           `create-mr [--draft] [--title=<title>] [-U|--update-related-descriptions] [--yes]`:
  
               Creates a MR for the current branch, using the upstream (parent) branch as the MR source branch.
               Once the MR is successfully created, annotates the current branch with the new MR's number.
@@ -831,7 +872,7 @@ long_docs: Dict[str, str] = {
 
               If `.git/info/reviewers` file is present, its contents (one GitLab login per line) are used to set reviewers.
 
-              The subject of the first unique commit of the branch is used as MR title.
+              Unless `--title` is provided, the subject of the first unique commit of the branch is used as MR title.
               If `.git/info/description` or `.gitlab/merge_request_templates/Default.md` template is present, its contents are used as MR description.
               Otherwise (or if `machete.gitlab.forceDescriptionFromCommitMessage` is set), MR description is taken from message body of the first unique commit of the branch.
 
@@ -849,10 +890,15 @@ long_docs: Dict[str, str] = {
 
               Set the MR title explicitly (the default is to use the first included commit's message as the title).
 
+                 <b>-U</b>, <b>--update-related-descriptions</b>
+
+              Update the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs.
+              See help for `git machete gitlab update-mr-descriptions --related` for details.
+
                  <b>--yes</b>
               Do not ask for confirmation whether to push the branch.
 
-           `restack-mr`:
+           `restack-mr [-U|--update-related-descriptions]`:
  
               Perform the following sequence of actions:
 
@@ -867,7 +913,14 @@ long_docs: Dict[str, str] = {
               The drafting/undrafting is useful in case the GitLab project has set up code owners.
               Draft MRs don't get code owners automatically added as reviewers.
 
-           `retarget-mr [-b|--branch=<branch>] [--ignore-if-missing]`:
+              <b>Options:</b>
+
+                 <b>-U</b>, <b>--update-related-descriptions</b>
+
+              Update the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs.
+              See help for `git machete gitlab update-mr-descriptions --related` for details.
+
+           `retarget-mr [-b|--branch=<branch>] [--ignore-if-missing] [-U|--update-related-descriptions]`:
  
               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`).
 
@@ -886,6 +939,29 @@ long_docs: Dict[str, str] = {
 
               Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch.
 
+                 <b>-U</b>, <b>--update-related-descriptions</b>
+
+              Update the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs.
+              See help for `git machete gitlab update-mr-descriptions --related` for details.
+
+           `update-mr-descriptions`:
+ 
+              Updates the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs
+              (depending on `machete.gitlab.mrDescriptionIntroStyle` git config key).
+
+              <b>Options:</b>
+
+                 <b>--all</b>
+              Update MR descriptions for all MRs in the project.
+
+                 <b>--mine</b>
+              Update MR descriptions for all MRs opened by the current user associated with the GitLab token.
+
+                 <b>--related</b>
+              Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch.
+              If `machete.gitlab.mrDescriptionIntroStyle` is `up-only` (default), then only downstream MR descriptions are updated.
+              If `machete.gitlab.mrDescriptionIntroStyle` is `full`, then both downstream and upstream MR descriptions are updated.
+
         <b>Git config keys:</b>
            `machete.gitlab.{domain,remote,namespace,project}` (all subcommands):
  
@@ -1486,6 +1562,12 @@ long_docs: Dict[str, str] = {
 
                  * whether `traverse` suggests to slide out the branch.
 
+           `machete.traverse.fetch.<remote>`:
+              Configures the behavior of `git machete traverse` command for the given remote when `--fetch` flag is used.
+              If set to `false`, this remote will not be fetched before the traversal.
+              The default value of this configuration is `true`.
+              This is useful for excluding remotes that are temporarily offline, or take a long time to respond.
+
            `machete.traverse.push`
  
               To change the behavior of `git machete traverse` command so that it doesn't push branches by default,
diff --git a/git_machete/git_config_keys.py b/git_machete/git_config_keys.py
index 88dc170..a44e004 100644
--- a/git_machete/git_config_keys.py
+++ b/git_machete/git_config_keys.py
@@ -10,3 +10,7 @@ def override_fork_point_to(branch: str) -> str:
 
 def override_fork_point_while_descendant_of(branch: str) -> str:
     return f'machete.overrideForkPoint.{branch}.whileDescendantOf'
+
+
+def traverse_remote_fetch(remote: str) -> str:
+    return f'machete.traverse.fetch.{remote}'
diff --git a/git_machete/options.py b/git_machete/options.py
index 68a0996..2c19879 100644
--- a/git_machete/options.py
+++ b/git_machete/options.py
@@ -7,18 +7,22 @@ from .git_operations import AnyBranchName, AnyRevision, LocalBranchShortName
 class CommandLineOptions:
 
     def __init__(self) -> None:
+        self.opt_all: bool = False
         self.opt_as_first_child: bool = False
         self.opt_as_root: bool = False
         self.opt_branch: Optional[AnyBranchName] = None
+        self.opt_by: Optional[str] = None
         self.opt_checked_out_since: Optional[str] = None
         self.opt_delete: bool = False
         self.opt_down_fork_point: Optional[AnyRevision] = None
         self.opt_draft: bool = False
         self.opt_fetch: bool = False
         self.opt_fork_point: Optional[AnyRevision] = None
+        self.opt_ignore_if_missing: bool = False
         self.opt_inferred: bool = False
         self.opt_list_commits: bool = False
         self.opt_list_commits_with_hashes: bool = False
+        self.opt_mine: bool = False
         self.opt_merge: bool = False
         self.opt_n: bool = False
         self.opt_no_edit_merge: bool = False
@@ -29,6 +33,7 @@ class CommandLineOptions:
         self.opt_override_to_parent: bool = False
         self.opt_push_tracked: bool = True
         self.opt_push_untracked: bool = True
+        self.opt_related: bool = False
         self.opt_removed_from_remote: bool = False
         self.opt_return_to: str = "stay"
         self.opt_roots: List[LocalBranchShortName] = list()
@@ -40,6 +45,7 @@ class CommandLineOptions:
         self.opt_sync_gitlab_mrs: bool = False
         self.opt_title: Optional[str] = None
         self.opt_unset_override: bool = False
+        self.opt_update_related_descriptions: bool = False
         self.opt_with_urls: bool = False
         self.opt_yes: bool = False
 
diff --git a/requirements/coverage.txt b/requirements/coverage.txt
index 487a728..62a99b8 100644
--- a/requirements/coverage.txt
+++ b/requirements/coverage.txt
@@ -1,7 +1,9 @@
 coverage==6.2; python_version <= '3.6'
 coverage==7.2.7; python_version >= '3.7' and python_version < '3.8'
-coverage==7.6.1; python_version >= '3.8'
+coverage==7.6.1; python_version >= '3.8' and python_version < '3.9'
+coverage==7.6.4; python_version >= '3.9'
 
 pytest-cov==4.0.0; python_version <= '3.6'
 pytest-cov==4.1.0; python_version >= '3.7' and python_version < '3.8'
-pytest-cov==5.0.0; python_version >= '3.8'
+pytest-cov==5.0.0; python_version >= '3.8' and python_version < '3.9'
+pytest-cov==6.0.0; python_version >= '3.9'
diff --git a/requirements/mypy.txt b/requirements/mypy.txt
index 88af65e..2c98de8 100644
--- a/requirements/mypy.txt
+++ b/requirements/mypy.txt
@@ -1,3 +1,3 @@
 mypy==0.971; python_version <= '3.6'
 mypy==1.4.1; python_version >= '3.7' and python_version < '3.8'
-mypy==1.11.2; python_version >= '3.8'
+mypy==1.13.0; python_version >= '3.8'
diff --git a/requirements/pypi-publish.txt b/requirements/pypi-publish.txt
index 4bf8977..d853110 100644
--- a/requirements/pypi-publish.txt
+++ b/requirements/pypi-publish.txt
@@ -1,4 +1,4 @@
 build==1.2.2.post1
-setuptools==75.1.0
+setuptools==75.3.0
 twine==5.1.1
 wheel==0.44.0
diff --git a/requirements/sphinx-docs.txt b/requirements/sphinx-docs.txt
index 1f34b53..6a7b8c5 100644
--- a/requirements/sphinx-docs.txt
+++ b/requirements/sphinx-docs.txt
@@ -1,7 +1,7 @@
 # Dependency of `sphinx-book-theme`.
 # Added explicitly after https://github.com/pydata/pydata-sphinx-theme/pull/1503
 # broke the build despite no explicit update to deps.
-pydata-sphinx-theme==0.15.4
+pydata-sphinx-theme==0.16.0
 
-sphinx==8.0.2
+sphinx==8.1.3
 sphinx-book-theme==1.1.3
diff --git a/requirements/typos.txt b/requirements/typos.txt
index 9ec4b1f..72bae3f 100644
--- a/requirements/typos.txt
+++ b/requirements/typos.txt
@@ -1,1 +1,1 @@
-typos==1.25.0
+typos==1.27.0
diff --git a/tox.ini b/tox.ini
index ae93af5..2d3c555 100644
--- a/tox.ini
+++ b/tox.ini
@@ -19,8 +19,9 @@ deps =
 allowlist_externals = mkdir
 commands =
   mkdir -p test-results/
-  # `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}
+  # `python -m pytest` so that the local plugin for `--full-operands` 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"
@@ -87,8 +88,9 @@ deps =
 passenv = PYTHON_VERSION
 allowlist_externals = cp
 commands =
+  # `python -m pytest` so that the local plugin for `--full-operands` can be loaded, see https://stackoverflow.com/a/48306599
   # Generate (or append results to the existing) .coverage binary file (SQLite database)
-  pytest --cov=git_machete --cov-append --cov-branch \
+  python -m pytest --cov=git_machete --cov-append --cov-branch --cov-context=test -p tests.pytest_full_operands \
     --numprocesses=auto --junitxml=test-results/testenv-{envname}.xml -m "not completion_e2e" {posargs}
   # Save a report to htmlcov/
   coverage html --show-contexts
@@ -106,7 +108,6 @@ commands =
   sh -c "rm -f {toxinidir}/.coverage.*"
 
 [coverage:run]
-dynamic_context = test_function
 omit = tests/*,git_machete/bin.py
 relative_files = True
 


Test patch:
diff --git a/tests/completion_e2e/test_completion_e2e.py b/tests/completion_e2e/test_completion_e2e.py
index 5f94fc5..9ba472f 100644
--- a/tests/completion_e2e/test_completion_e2e.py
+++ b/tests/completion_e2e/test_completion_e2e.py
@@ -59,29 +59,37 @@ test_cases: Dict[str, str] = {
     "git machete fork-point --unset-override ":
         "develop",
     "git machete github ":
-        "anno-prs checkout-prs create-pr restack-pr retarget-pr",
+        "anno-prs checkout-prs create-pr restack-pr retarget-pr update-pr-descriptions",
     "git machete github anno-prs -":
         "--debug -h --help -v --verbose --with-urls",
     "git machete github checkout-prs -":
         "--all --by --debug -h --help --mine -v --verbose",
     "git machete github create-pr -":
-        "--debug --draft -h --help --title -v --verbose --yes",
+        "-U --debug --draft -h --help --title --update-related-descriptions -v --verbose --yes",
+    "git machete github restack-pr -":
+        "-U --debug -h --help --update-related-descriptions -v --verbose",
     "git machete github retarget-pr --":
-        "--branch --debug --help --ignore-if-missing --verbose",
+        "--branch --debug --help --ignore-if-missing --update-related-descriptions --verbose",
     "git machete github retarget-pr -b ":
         "develop master",
+    "git machete github update-pr-descriptions -":
+        "--all --debug -h --help --mine --related -v --verbose",
     "git machete gitlab ":
-        "anno-mrs checkout-mrs create-mr restack-mr retarget-mr",
+        "anno-mrs checkout-mrs create-mr restack-mr retarget-mr update-mr-descriptions",
     "git machete gitlab anno-mrs -":
         "--debug -h --help -v --verbose --with-urls",
     "git machete gitlab checkout-mrs -":
         "--all --by --debug -h --help --mine -v --verbose",
     "git machete gitlab create-mr -":
-        "--debug --draft -h --help --title -v --verbose --yes",
+        "-U --debug --draft -h --help --title --update-related-descriptions -v --verbose --yes",
+    "git machete gitlab restack-mr -":
+        "-U --debug -h --help --update-related-descriptions -v --verbose",
     "git machete gitlab retarget-mr --":
-        "--branch --debug --help --ignore-if-missing --verbose",
+        "--branch --debug --help --ignore-if-missing --update-related-descriptions --verbose",
     "git machete gitlab retarget-mr -b ":
         "develop master",
+    "git machete gitlab update-mr-descriptions -":
+        "--all --debug -h --help --mine --related -v --verbose",
     "git machete g ":
         "down first last next prev root up",
     "git machete go ":
diff --git a/tests/mockers_github.py b/tests/mockers_github.py
index 8e95b05..24a3c7c 100644
--- a/tests/mockers_github.py
+++ b/tests/mockers_github.py
@@ -146,7 +146,7 @@ def __mock_urlopen_impl(github_api_state: MockGitHubAPIState, request: Request)
                 return MockAPIResponse(HTTPStatus.OK, pull)
             raise error_404()
         elif url_path_matches('/user'):
-            return MockAPIResponse(HTTPStatus.OK, {'login': 'github_user', 'type': 'User', 'company': 'VirtusLab'})
+            return MockAPIResponse(HTTPStatus.OK, {'login': 'github_user', 'type': 'User'})
         else:
             raise error_404()
 
diff --git a/tests/test_github.py b/tests/test_github.py
index b300cd8..8ee3776 100644
--- a/tests/test_github.py
+++ b/tests/test_github.py
@@ -335,9 +335,9 @@ class TestGitHub(BaseTest):
         assert_failure(["github", "anno-prs", "--draft"],
                        "--draft option is only valid with create-pr subcommand.")
         assert_failure(["github", "anno-prs", "--mine"],
-                       "--mine option is only valid with checkout-prs subcommand.")
+                       "--mine option is only valid with checkout-prs and update-pr-descriptions subcommands.")
         assert_failure(["github", "retarget-pr", "123"],
-                       "request_id option is only valid with checkout-prs subcommand.")
+                       "PR number is only valid with checkout-prs subcommand.")
         assert_failure(["github", "checkout-prs", "-b", "foo"],
                        "--branch option is only valid with retarget-pr subcommand.")
         assert_failure(["github", "create-pr", "--ignore-if-missing"],
@@ -350,3 +350,11 @@ class TestGitHub(BaseTest):
                        "--with-urls option is only valid with anno-prs subcommand.")
         assert_failure(["github", "restack-pr", "--yes"],
                        "--yes option is only valid with create-pr subcommand.")
+        assert_failure(["github", "update-pr-descriptions", "--by=other-user"],
+                       "--by option is only valid with checkout-prs subcommand.")
+        assert_failure(["github", "checkout-prs", "--related"],
+                       "--related option is only valid with update-pr-descriptions subcommand.")
+        assert_failure(["github", "update-pr-descriptions", "--all", "--related"],
+                       "update-pr-descriptions subcommand must take exactly one of the following options: --all, --mine, --related")
+        assert_failure(["github", "update-pr-descriptions", "--update-related-descriptions"],
+                       "--update-related-descriptions option is only valid with create-pr, restack-pr and retarget-pr subcommands.")
diff --git a/tests/test_github_checkout_prs.py b/tests/test_github_checkout_prs.py
index 113063b..8783aac 100644
--- a/tests/test_github_checkout_prs.py
+++ b/tests/test_github_checkout_prs.py
@@ -523,7 +523,7 @@ class TestGitHubCheckoutPRs(BaseTest):
             ["github", "checkout-prs", "--mine"],
             """
             Checking for open GitHub PRs... OK
-            Warn: Current user github_user has no open pull request in repository example-org/example-repo
+            Warn: User github_user has no open pull request in repository example-org/example-repo
             """
         )
 
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index 1e6b240..f0829be 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -145,7 +145,7 @@ class TestGitHubCreatePR(BaseTest):
 
             Checking if base branch ignore-trailing exists in origin remote... YES
             Creating a draft PR from chore/fields to ignore-trailing... OK, see www.github.com
-            Checking for open GitHub PRs (to determine PR chain)... OK
+            Checking for open GitHub PRs... OK
             Updating description of PR #5 to include the chain of PRs... OK
             Setting milestone of PR #5 to 42... OK
             Adding github_user as assignee to PR #5... OK
@@ -208,29 +208,66 @@ class TestGitHubCreatePR(BaseTest):
         )
 
         # diverged from and newer than origin
-        launch_command("github", "create-pr")
         assert_success(
-            ['status'],
+            ["github", "create-pr", "-U"],
             """
-            master
-            |
-            o-hotfix/add-trigger *  PR #6 (some_other_user)
+            Branch hotfix/add-trigger diverged from (and has newer commits than) its remote counterpart origin/hotfix/add-trigger.
+            Push hotfix/add-trigger with force-with-lease to origin? (y, N, q)
+
+              master
               |
-              x-ignore-trailing  PR #3 (diverged from & older than origin)
+              o-hotfix/add-trigger *
                 |
-                o-chore/fields  PR #5 (some_other_user)
+                x-ignore-trailing  PR #3 (diverged from & older than origin)
+                  |
+                  o-chore/fields  PR #5 (some_other_user)
 
-            develop
-            |
-            x-allow-ownership-link (ahead of origin)
-            | |
-            | x-build-chain (untracked)
-            |
-            o-call-ws  PR #4 (some_other_user)
+              develop
               |
-              x-drop-constraint (untracked)
-            """,
+              x-allow-ownership-link (ahead of origin)
+              | |
+              | x-build-chain (untracked)
+              |
+              o-call-ws  PR #4 (some_other_user)
+                |
+                x-drop-constraint (untracked)
+
+            Checking if base branch master exists in origin remote... YES
+            Creating a PR from hotfix/add-trigger to master... OK, see www.github.com
+            Updating description of PR #6 to include the chain of PRs... OK
+            Setting milestone of PR #6 to 42... OK
+            Adding github_user as assignee to PR #6... OK
+            Adding foo, bar as reviewers to PR #6... OK
+            Updating descriptions of other PRs...
+            Checking for open GitHub PRs... OK
+            Description of PR #3 (ignore-trailing -> hotfix/add-trigger) has been updated
+            Description of PR #5 (chore/fields -> ignore-trailing) has been updated
+            """
         )
+        pr5 = github_api_state.get_pull_by_number(5)
+        assert pr5 is not None
+        assert pr5["body"] == textwrap.dedent("""
+            # PR title
+            ## Summary
+            ## Test plan
+
+            <!-- start git-machete generated -->
+
+            # Based on PR #3
+
+            ## Chain of upstream PRs as of 2023-12-31
+
+            * PR #6:
+              `master` ← `hotfix/add-trigger`
+
+              * PR #3:
+                `hotfix/add-trigger` ← `ignore-trailing`
+
+                * **PR #5 (THIS ONE)**:
+                  `ignore-trailing` ← `chore/fields`
+
+            <!-- end git-machete generated -->""")[1:]
+
         expected_error_message = "A pull request already exists for test_repo:hotfix/add-trigger."
         assert_failure(["github", "create-pr"], expected_error_message)
 
@@ -586,7 +623,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch branch-1 exists in origin_2 remote... YES
         Creating a PR from feature to branch-1... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #16 to include the chain of PRs... OK
         """  # noqa: E501
 
@@ -649,7 +686,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch feature exists in origin_1 remote... YES
         Creating a PR from feature_1 to feature... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #17 to include the chain of PRs... OK
         """
         assert_success(
@@ -687,7 +724,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch feature exists in origin_1 remote... YES
         Creating a PR from feature_2 to feature... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #18 to include the chain of PRs... OK
         """
         assert_success(
@@ -709,7 +746,7 @@ class TestGitHubCreatePR(BaseTest):
         Added branch feature_3 onto feature_2
         Checking if base branch feature_2 exists in origin_1 remote... YES
         Creating a PR from feature_3 to feature_2... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #19 to include the chain of PRs... OK
         """  # noqa: E501
         assert_success(
@@ -740,7 +777,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch feature_3 exists in origin_1 remote... YES
         Creating a PR from feature_4 to feature_3... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #20 to include the chain of PRs... OK
         """
         assert_success(
@@ -772,7 +809,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Checking if base branch feature_3 exists in origin_1 remote... YES
         Creating a PR from feature_5 to feature_3... OK, see www.github.com
-        Checking for open GitHub PRs (to determine PR chain)... OK
+        Checking for open GitHub PRs... OK
         Updating description of PR #21 to include the chain of PRs... OK
         """
         assert_success(
@@ -954,3 +991,35 @@ class TestGitHubCreatePR(BaseTest):
             Creating a PR from feature to develop... OK, see www.github.com
             """
         )
+
+    def test_github_create_pr_with_title_from_file(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none)
+        github_api_state = MockGitHubAPIState.with_prs()
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(github_api_state))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop").commit("Some commit").push()
+            .new_branch("feature").commit("Add feature").push()
+        )
+
+        rewrite_branch_layout_file("develop\n\tfeature")
+
+        pr_title = "Feature Implementation"
+        self.repo_sandbox.write_to_file(".git/info/title", pr_title)
+
+        launch_command("github", "create-pr")
+
+        pr = github_api_state.get_pull_by_number(1)
+        assert pr is not None
+        assert pr['title'] == pr_title
+
+        assert_success(
+            ['status'],
+            """
+            develop
+            |
+            o-feature *  PR #1 (some_other_user)
+            """,
+        )
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index ea7e18d..50ebb99 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -253,7 +253,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #20 has been switched to feature\n'
-            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
+            'Checking for open GitHub PRs... OK\n'
             'Description of PR #20 has been updated\n'
         )
         pr20 = github_api_state.get_pull_by_number(20)
@@ -335,7 +335,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #30 has been switched to feature_2\n'
-            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
+            'Checking for open GitHub PRs... OK\n'
             'Description of PR #30 has been updated\n'
         )
         pr30 = github_api_state.get_pull_by_number(30)
@@ -375,7 +375,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #30 has been switched to feature\n'
-            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
+            'Checking for open GitHub PRs... OK\n'
             'Description of PR #30 has been updated\n'
         )
         pr30 = github_api_state.get_pull_by_number(30)
@@ -421,13 +421,32 @@ class TestGitHubRetargetPR(BaseTest):
 
         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'
+            ['github', 'retarget-pr', '-U'],
+            """
+            Base branch of PR #15 has been switched to branch-1
+            Updating descriptions of other PRs...
+            Checking for open GitHub PRs... OK
+            Description of PR #20 (feature_1 -> feature) has been updated
+            Description of PR #25 (feature_2 -> feature) has been updated
+            Description of PR #35 (feature_4 -> feature) has been updated
+            """
+        )
+        pr15 = github_api_state.get_pull_by_number(15)
+        assert pr15 is not None
+        assert pr15['base']['ref'] == 'branch-1'
+        assert pr15['body'] == '# Summary'
+
+        self.repo_sandbox.set_git_config_key("machete.github.prDescriptionIntroStyle", "full")
+        assert_success(
+            ['github', 'retarget-pr', '-U'],
+            """
+            Base branch of PR #15 is already branch-1
+            Checking for open GitHub PRs... OK
+            Description of PR #15 has been updated
+            Updating descriptions of other PRs...
+            """
         )
         pr15 = github_api_state.get_pull_by_number(15)
         assert pr15 is not None
@@ -435,7 +454,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert pr15['body'] == textwrap.dedent('''
             <!-- start git-machete generated -->
 
-            ## Chain of upstream PRs & tree of downstream PRs as of 2023-12-31
+            ## Tree of downstream PRs as of 2023-12-31
 
             * **PR #15 (THIS ONE)**:
               `branch-1` ← `feature`
diff --git a/tests/test_github_update_pr_descriptions.py b/tests/test_github_update_pr_descriptions.py
new file mode 100644
index 0000000..487b179
--- /dev/null
+++ b/tests/test_github_update_pr_descriptions.py
@@ -0,0 +1,221 @@
+import textwrap
+from typing import Any, Dict, List
+
+from pytest_mock import MockerFixture
+
+from tests.base_test import BaseTest
+from tests.mockers import (assert_failure, assert_success,
+                           rewrite_branch_layout_file)
+from tests.mockers_code_hosting import mock_from_url
+from tests.mockers_github import (MockGitHubAPIState,
+                                  mock_github_token_for_domain_fake,
+                                  mock_github_token_for_domain_none,
+                                  mock_pr_json, mock_urlopen)
+
+
+class TestGitHubUpdatePRDescriptions(BaseTest):
+    @staticmethod
+    def prs_for_test_update_pr_descriptions() -> List[Dict[str, Any]]:
+        return [
+            mock_pr_json(head='chore/redundant_checks', base='restrict_access', number=18),
+            mock_pr_json(head='restrict_access', base='allow-ownership-link', number=17, user='github_user'),
+            mock_pr_json(head='allow-ownership-link', base='bugfix/feature', number=12),
+            mock_pr_json(head='bugfix/feature', base='enhance/feature', number=6),
+            mock_pr_json(head='enhance/add_user', base='develop', number=19),
+            mock_pr_json(head='testing/add_user', base='bugfix/add_user', number=22),
+            mock_pr_json(head='chore/comments', base='testing/add_user', number=24),
+            mock_pr_json(head='ignore-trailing', base='hotfix/add-trigger', number=3),
+            mock_pr_json(head='bugfix/remove-n-option', base='develop', number=5, state='closed', repo_id=2)
+        ]
+
+    def test_github_update_pr_descriptions(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake)
+        github_api_state = MockGitHubAPIState.with_prs(*self.prs_for_test_update_pr_descriptions())
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(github_api_state))
+        self.patch_symbol(mocker, 'git_machete.utils.get_current_date', lambda: '2023-12-31')
+
+        (
+            self.repo_sandbox.new_branch("root")
+            .commit("initial commit")
+            .new_branch("develop")
+            .commit("first commit")
+            .push()
+            .new_branch("enhance/feature")
+            .commit("introduce feature")
+            .push()
+            .new_branch("bugfix/feature")
+            .commit("bugs removed")
+            .push()
+            .new_branch("allow-ownership-link")
+            .commit("fixes")
+            .push()
+            .new_branch('restrict_access')
+            .commit('authorized users only')
+            .push()
+            .new_branch("chore/redundant_checks")
+            .commit('remove some checks')
+            .push()
+            .check_out("root")
+            .new_branch("master")
+            .commit("Master commit")
+            .push()
+            .new_branch("hotfix/add-trigger")
+            .commit("HOTFIX Add the trigger")
+            .push()
+            .new_branch("ignore-trailing")
+            .commit("Ignore trailing data")
+            .push()
+            .delete_branch("root")
+            .new_branch('chore/fields')
+            .commit("remove outdated fields")
+            .push()
+            .check_out('develop')
+            .new_branch('enhance/add_user')
+            .commit('allow externals to add users')
+            .push()
+            .new_branch('bugfix/add_user')
+            .commit('first round of fixes')
+            .push()
+            .new_branch('testing/add_user')
+            .commit('add test set for add_user feature')
+            .push()
+            .new_branch('chore/comments')
+            .commit('code maintenance')
+            .push()
+            .check_out('allow-ownership-link')
+        )
+
+        body: str = \
+            """
+            develop
+              enhance/feature
+                bugfix/feature
+                  allow-ownership-link
+                    restrict_access
+                      chore/redundant_checks
+            bugfix/add_user
+              testing/add_user
+                chore/comments
+            """
+        rewrite_branch_layout_file(body)
+
+        assert_success(
+            ['github', 'update-pr-descriptions', '--mine'],
+            """
+            Checking for open GitHub PRs... OK
+            Description of PR #17 (restrict_access -> allow-ownership-link) has been updated
+            """
+        )
+
+        assert_success(
+            ['github', 'update-pr-descriptions', '--related'],
+            """
+            Checking for open GitHub PRs... OK
+            Description of PR #18 (chore/redundant_checks -> restrict_access) has been updated
+            """
+        )
+        self.repo_sandbox.set_git_config_key("machete.github.prDescriptionIntroStyle", "full")
+        assert_success(
+            ['github', 'update-pr-descriptions', '--related'],
+            """
+            Checking for open GitHub PRs... OK
+            Description of PR #6 (bugfix/feature -> enhance/feature) has been updated
+            Description of PR #12 (allow-ownership-link -> bugfix/feature) has been updated
+            Description of PR #17 (restrict_access -> allow-ownership-link) has been updated
+            """
+        )
+
+        assert_success(
+            ['github', 'update-pr-descriptions', '--all'],
+            """
+            Checking for open GitHub PRs... OK
+            Description of PR #22 (testing/add_user -> bugfix/add_user) has been updated
+            Description of PR #24 (chore/comments -> testing/add_user) has been updated
+            """
+        )
+
+        pr = github_api_state.get_pull_by_number(6)
+        assert pr is not None
+        assert pr['body'] == textwrap.dedent("""
+            <!-- start git-machete generated -->
+
+            ## Tree of downstream PRs as of 2023-12-31
+
+            * **PR #6 (THIS ONE)**:
+              `enhance/feature` ← `bugfix/feature`
+
+                * PR #12:
+                  `bugfix/feature` ← `allow-ownership-link`
+
+                  * PR #17:
+                    `allow-ownership-link` ← `restrict_access`
+
+                    * PR #18:
+                      `restrict_access` ← `chore/redundant_checks`
+
+            <!-- end git-machete generated -->
+
+            # Summary""")[1:]
+
+        pr = github_api_state.get_pull_by_number(12)
+        assert pr is not None
+        assert pr['body'] == textwrap.dedent("""
+            <!-- start git-machete generated -->
+
+            # Based on PR #6
+
+            ## Chain of upstream PRs & tree of downstream PRs as of 2023-12-31
+
+            * PR #6:
+              `enhance/feature` ← `bugfix/feature`
+
+              * **PR #12 (THIS ONE)**:
+                `bugfix/feature` ← `allow-ownership-link`
+
+                  * PR #17:
+                    `allow-ownership-link` ← `restrict_access`
+
+                    * PR #18:
+                      `restrict_access` ← `chore/redundant_checks`
+
+            <!-- end git-machete generated -->
+
+            # Summary""")[1:]
+
+        pr = github_api_state.get_pull_by_number(19)
+        assert pr is not None
+        assert pr['body'] == "# Summary"
+
+    def test_github_update_pr_descriptions_misc_failures_and_warns(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitHubAPIState.with_prs()))
+
+        self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none)
+        assert_success(
+            ["github", "update-pr-descriptions", "--all"],
+            """
+            Checking for open GitHub PRs... OK
+            Warn: Currently there are no pull requests opened in repository example-org/example-repo
+            """
+        )
+
+        assert_failure(
+            ["github", "update-pr-descriptions", "--mine"],
+            """
+            Could not determine current user name, please check that the GitHub API token provided by one of the:
+            \t1. GITHUB_TOKEN environment variable
+            \t2. Content of the ~/.github-token file
+            \t3. Current auth token from the gh GitHub CLI
+            \t4. Current auth token from the hub GitHub CLI
+            is valid."""
+        )
+
+        self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake)
+        assert_success(
+            ["github", "update-pr-descriptions", "--mine"],
+            """
+            Checking for open GitHub PRs... OK
+            Warn: User github_user has no open pull request in repository example-org/example-repo
+            """
+        )
diff --git a/tests/test_gitlab.py b/tests/test_gitlab.py
index e426576..233bbf1 100644
--- a/tests/test_gitlab.py
+++ b/tests/test_gitlab.py
@@ -285,9 +285,9 @@ class TestGitLab(BaseTest):
         assert_failure(["gitlab", "anno-mrs", "--draft"],
                        "--draft option is only valid with create-mr subcommand.")
         assert_failure(["gitlab", "anno-mrs", "--mine"],
-                       "--mine option is only valid with checkout-mrs subcommand.")
+                       "--mine option is only valid with checkout-mrs and update-mr-descriptions subcommands.")
         assert_failure(["gitlab", "retarget-mr", "123"],
-                       "request_id option is only valid with checkout-mrs subcommand.")
+                       "MR number is only valid with checkout-mrs subcommand.")
         assert_failure(["gitlab", "checkout-mrs", "-b", "foo"],
                        "--branch option is only valid with retarget-mr subcommand.")
         assert_failure(["gitlab", "create-mr", "--ignore-if-missing"],
@@ -300,3 +300,11 @@ class TestGitLab(BaseTest):
                        "--with-urls option is only valid with anno-mrs subcommand.")
         assert_failure(["gitlab", "restack-mr", "--yes"],
                        "--yes option is only valid with create-mr subcommand.")
+        assert_failure(["gitlab", "update-mr-descriptions", "--by=other-user"],
+                       "--by option is only valid with checkout-mrs subcommand.")
+        assert_failure(["gitlab", "checkout-mrs", "--related"],
+                       "--related option is only valid with update-mr-descriptions subcommand.")
+        assert_failure(["gitlab", "update-mr-descriptions", "--all", "--related"],
+                       "update-mr-descriptions subcommand must take exactly one of the following options: --all, --mine, --related")
+        assert_failure(["gitlab", "update-mr-descriptions", "--update-related-descriptions"],
+                       "--update-related-descriptions option is only valid with create-mr, restack-mr and retarget-mr subcommands.")
diff --git a/tests/test_gitlab_checkout_mrs.py b/tests/test_gitlab_checkout_mrs.py
index a9c35e1..cfbac8f 100644
--- a/tests/test_gitlab_checkout_mrs.py
+++ b/tests/test_gitlab_checkout_mrs.py
@@ -524,7 +524,7 @@ class TestGitLabCheckoutMRs(BaseTest):
             ["gitlab", "checkout-mrs", "--mine"],
             """
             Checking for open GitLab MRs... OK
-            Warn: Current user gitlab_user has no open merge request in project example-org/example-repo
+            Warn: User gitlab_user has no open merge request in project example-org/example-repo
             """
         )
 
diff --git a/tests/test_gitlab_create_mr.py b/tests/test_gitlab_create_mr.py
index 30e8d23..ad93a22 100644
--- a/tests/test_gitlab_create_mr.py
+++ b/tests/test_gitlab_create_mr.py
@@ -144,7 +144,7 @@ class TestGitLabCreateMR(BaseTest):
 
             Checking if target branch ignore-trailing exists in origin remote... YES
             Creating a draft MR from chore/fields to ignore-trailing... OK, see www.gitlab.com
-            Checking for open GitLab MRs (to determine MR chain)... OK
+            Checking for open GitLab MRs... OK
             Updating description of MR !5 to include the chain of MRs... OK
             Setting milestone of MR !5 to 42... OK
             Adding gitlab_user as assignee to MR !5... OK
@@ -206,29 +206,65 @@ class TestGitLabCreateMR(BaseTest):
         )
 
         # diverged from and newer than origin
-        launch_command("gitlab", "create-mr")
         assert_success(
-            ['status'],
+            ["gitlab", "create-mr", "--update-related-descriptions"],
             """
-            master
-            |
-            o-hotfix/add-trigger *  MR !6 (some_other_user)
+            Branch hotfix/add-trigger diverged from (and has newer commits than) its remote counterpart origin/hotfix/add-trigger.
+            Push hotfix/add-trigger with force-with-lease to origin? (y, N, q)
+
+              master
               |
-              x-ignore-trailing  MR !3 (diverged from & older than origin)
+              o-hotfix/add-trigger *
                 |
-                o-chore/fields  MR !5 (some_other_user)
+                x-ignore-trailing  MR !3 (diverged from & older than origin)
+                  |
+                  o-chore/fields  MR !5 (some_other_user)
 
-            develop
-            |
-            x-allow-ownership-link (ahead of origin)
-            | |
-            | x-build-chain (untracked)
-            |
-            o-call-ws  MR !4 (some_other_user)
+              develop
               |
-              x-drop-constraint (untracked)
-            """,
-        )
+              x-allow-ownership-link (ahead of origin)
+              | |
+              | x-build-chain (untracked)
+              |
+              o-call-ws  MR !4 (some_other_user)
+                |
+                x-drop-constraint (untracked)
+
+            Checking if target branch master exists in origin remote... YES
+            Creating a MR from hotfix/add-trigger to master... OK, see www.gitlab.com
+            Updating description of MR !6 to include the chain of MRs... OK
+            Setting milestone of MR !6 to 42... OK
+            Adding gitlab_user as assignee to MR !6... OK
+            Adding foo, bar as reviewers to MR !6... OK
+            Updating descriptions of other MRs...
+            Checking for open GitLab MRs... OK
+            Description of MR !3 (ignore-trailing -> hotfix/add-trigger) has been updated
+            Description of MR !5 (chore/fields -> ignore-trailing) has been updated
+        """)
+        mr5 = gitlab_api_state.get_mr_by_number(5)
+        assert mr5 is not None
+        assert mr5["description"] == textwrap.dedent("""
+            # MR title
+            ## Summary
+            ## Test plan
+
+            <!-- start git-machete generated -->
+
+            # Based on MR !3
+
+            ## Chain of upstream MRs as of 2023-12-31
+
+            * MR !6:
+              `master` ← `hotfix/add-trigger`
+
+              * MR !3:
+                `hotfix/add-trigger` ← `ignore-trailing`
+
+                * **MR !5 (THIS ONE)**:
+                  `ignore-trailing` ← `chore/fields`
+
+            <!-- end git-machete generated -->""")[1:]
+
         expected_error_message = "Another open merge request already exists for this source branch: !6"
         assert_failure(["gitlab", "create-mr"], expected_error_message)
 
@@ -566,7 +602,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch branch-1 exists in origin_2 remote... YES
         Creating a MR from feature to branch-1... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !16 to include the chain of MRs... OK
         """  # noqa: E501
 
@@ -629,7 +665,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch feature exists in origin_1 remote... YES
         Creating a MR from feature_1 to feature... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !17 to include the chain of MRs... OK
         """
         assert_success(
@@ -667,7 +703,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch feature exists in origin_1 remote... YES
         Creating a MR from feature_2 to feature... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !18 to include the chain of MRs... OK
         """
         assert_success(
@@ -689,7 +725,7 @@ class TestGitLabCreateMR(BaseTest):
         Added branch feature_3 onto feature_2
         Checking if target branch feature_2 exists in origin_1 remote... YES
         Creating a MR from feature_3 to feature_2... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !19 to include the chain of MRs... OK
         """  # noqa: E501
         assert_success(
@@ -720,7 +756,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch feature_3 exists in origin_1 remote... YES
         Creating a MR from feature_4 to feature_3... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !20 to include the chain of MRs... OK
         """
         assert_success(
@@ -752,7 +788,7 @@ class TestGitLabCreateMR(BaseTest):
 
         Checking if target branch feature_3 exists in origin_1 remote... YES
         Creating a MR from feature_5 to feature_3... OK, see www.gitlab.com
-        Checking for open GitLab MRs (to determine MR chain)... OK
+        Checking for open GitLab MRs... OK
         Updating description of MR !21 to include the chain of MRs... OK
         """
         assert_success(
@@ -934,3 +970,35 @@ class TestGitLabCreateMR(BaseTest):
             Creating a MR from feature to develop... OK, see www.gitlab.com
             """
         )
+
+    def test_gitlab_create_mr_with_title_from_file(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.gitlab.GitLabToken.for_domain', mock_gitlab_token_for_domain_none)
+        gitlab_api_state = MockGitLabAPIState.with_mrs()
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(gitlab_api_state))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop").commit("Some commit").push()
+            .new_branch("feature").commit("Add feature").push()
+        )
+
+        rewrite_branch_layout_file("develop\n\tfeature")
+
+        pr_title = "Feature Implementation"
+        self.repo_sandbox.write_to_file(".git/info/title", pr_title)
+
+        launch_command("gitlab", "create-mr")
+
+        pr = gitlab_api_state.get_mr_by_number(1)
+        assert pr is not None
+        assert pr['title'] == pr_title
+
+        assert_success(
+            ['status'],
+            """
+            develop
+            |
+            o-feature *  MR !1 (some_other_user)
+            """,
+        )
diff --git a/tests/test_gitlab_retarget_mr.py b/tests/test_gitlab_retarget_mr.py
index 0d218de..1ff4205 100644
--- a/tests/test_gitlab_retarget_mr.py
+++ b/tests/test_gitlab_retarget_mr.py
@@ -259,7 +259,7 @@ class TestGitLabRetargetMR(BaseTest):
         assert_success(
             ['gitlab', 'retarget-mr'],
             'Target branch of MR !20 has been switched to feature\n'
-            'Checking for open GitLab MRs (to determine MR chain)... OK\n'
+            'Checking for open GitLab MRs... OK\n'
             'Description of MR !20 has been updated\n'
         )
         mr20 = gitlab_api_state.get_mr_by_number(20)
@@ -310,7 +310,7 @@ class TestGitLabRetargetMR(BaseTest):
         assert_success(
             ['gitlab', 'retarget-mr'],
             'Target branch of MR !25 has been switched to feature\n'
-            'Checking for open GitLab MRs (to determine MR chain)... OK\n'
+            'Checking for open GitLab MRs... OK\n'
             'Description of MR !25 has been updated\n'
         )
         mr25 = gitlab_api_state.get_mr_by_number(25)
@@ -354,7 +354,7 @@ class TestGitLabRetargetMR(BaseTest):
         assert_success(
             ['gitlab', 'retarget-mr'],
             'Target branch of MR !30 has been switched to feature_2\n'
-            'Checking for open GitLab MRs (to determine MR chain)... OK\n'
+            'Checking for open GitLab MRs... OK\n'
             'Description of MR !30 has been updated\n'
         )
         mr30 = gitlab_api_state.get_mr_by_number(30)
@@ -394,7 +394,7 @@ class TestGitLabRetargetMR(BaseTest):
         assert_success(
             ['gitlab', 'retarget-mr'],
             'Target branch of MR !30 has been switched to feature\n'
-            'Checking for open GitLab MRs (to determine MR chain)... OK\n'
+            'Checking for open GitLab MRs... OK\n'
             'Description of MR !30 has been updated\n'
         )
         mr30 = gitlab_api_state.get_mr_by_number(30)
@@ -438,6 +438,59 @@ class TestGitLabRetargetMR(BaseTest):
         assert mr30['target_branch'] == 'root'
         assert mr30['description'] == '# Summary'
 
+        self.repo_sandbox.check_out('feature')
+        self.repo_sandbox.remove_remote('origin_2')
+
+        assert_success(
+            ['gitlab', 'retarget-mr', '-U'],
+            """
+            Target branch of MR !15 has been switched to branch-1
+            Updating descriptions of other MRs...
+            Checking for open GitLab MRs... OK
+            Description of MR !20 (feature_1 -> feature) has been updated
+            Description of MR !25 (feature_2 -> feature) has been updated
+            Description of MR !35 (feature_4 -> feature) has been updated
+            """
+        )
+        mr15 = gitlab_api_state.get_mr_by_number(15)
+        assert mr15 is not None
+        assert mr15['target_branch'] == 'branch-1'
+        assert mr15['description'] == '# Summary'
+
+        self.repo_sandbox.set_git_config_key("machete.gitlab.mrDescriptionIntroStyle", "full")
+        assert_success(
+            ['gitlab', 'retarget-mr', '-U'],
+            """
+            Target branch of MR !15 is already branch-1
+            Checking for open GitLab MRs... OK
+            Description of MR !15 has been updated
+            Updating descriptions of other MRs...
+            """
+        )
+        mr15 = gitlab_api_state.get_mr_by_number(15)
+        assert mr15 is not None
+        assert mr15['target_branch'] == 'branch-1'
+        assert mr15['description'] == textwrap.dedent('''
+            <!-- start git-machete generated -->
+
+            ## Tree of downstream MRs as of 2023-12-31
+
+            * **MR !15 (THIS ONE)**:
+              `branch-1` ← `feature`
+
+                * MR !20:
+                  `feature` ← `feature_1`
+
+                * MR !25:
+                  `feature` ← `feature_2`
+
+                * MR !35:
+                  `feature` ← `feature_4`
+
+            <!-- end git-machete generated -->
+
+            # Summary''')[1:]
+
     @staticmethod
     def gitlab_api_state_for_test_retarget_mr_root_branch() -> MockGitLabAPIState:
         return MockGitLabAPIState.with_mrs(
diff --git a/tests/test_gitlab_update_mr_descriptions.py b/tests/test_gitlab_update_mr_descriptions.py
new file mode 100644
index 0000000..9f2e3a3
--- /dev/null
+++ b/tests/test_gitlab_update_mr_descriptions.py
@@ -0,0 +1,220 @@
+import textwrap
+from typing import Any, Dict, List
+
+from pytest_mock import MockerFixture
+
+from tests.base_test import BaseTest
+from tests.mockers import (assert_failure, assert_success,
+                           rewrite_branch_layout_file)
+from tests.mockers_code_hosting import mock_from_url
+from tests.mockers_gitlab import (MockGitLabAPIState,
+                                  mock_gitlab_token_for_domain_fake,
+                                  mock_gitlab_token_for_domain_none,
+                                  mock_mr_json, mock_urlopen)
+
+
+class TestGitLabUpdateMRDescriptions(BaseTest):
+    @staticmethod
+    def mrs_for_test_update_mr_descriptions() -> List[Dict[str, Any]]:
+        return [
+            mock_mr_json(head='chore/redundant_checks', base='restrict_access', number=18),
+            mock_mr_json(head='restrict_access', base='allow-ownership-link', number=17, user='gitlab_user'),
+            mock_mr_json(head='allow-ownership-link', base='bugfix/feature', number=12),
+            mock_mr_json(head='bugfix/feature', base='enhance/feature', number=6),
+            mock_mr_json(head='enhance/add_user', base='develop', number=19),
+            mock_mr_json(head='testing/add_user', base='bugfix/add_user', number=22),
+            mock_mr_json(head='chore/comments', base='testing/add_user', number=24),
+            mock_mr_json(head='ignore-trailing', base='hotfix/add-trigger', number=3),
+            mock_mr_json(head='bugfix/remove-n-option', base='develop', number=5, state='closed', repo_id=2)
+        ]
+
+    def test_gitlab_update_mr_descriptions(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.gitlab.GitLabToken.for_domain', mock_gitlab_token_for_domain_fake)
+        gitlab_api_state = MockGitLabAPIState.with_mrs(*self.mrs_for_test_update_mr_descriptions())
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(gitlab_api_state))
+        self.patch_symbol(mocker, 'git_machete.utils.get_current_date', lambda: '2023-12-31')
+
+        (
+            self.repo_sandbox.new_branch("root")
+            .commit("initial commit")
+            .new_branch("develop")
+            .commit("first commit")
+            .push()
+            .new_branch("enhance/feature")
+            .commit("introduce feature")
+            .push()
+            .new_branch("bugfix/feature")
+            .commit("bugs removed")
+            .push()
+            .new_branch("allow-ownership-link")
+            .commit("fixes")
+            .push()
+            .new_branch('restrict_access')
+            .commit('authorized users only')
+            .push()
+            .new_branch("chore/redundant_checks")
+            .commit('remove some checks')
+            .push()
+            .check_out("root")
+            .new_branch("master")
+            .commit("Master commit")
+            .push()
+            .new_branch("hotfix/add-trigger")
+            .commit("HOTFIX Add the trigger")
+            .push()
+            .new_branch("ignore-trailing")
+            .commit("Ignore trailing data")
+            .push()
+            .delete_branch("root")
+            .new_branch('chore/fields')
+            .commit("remove outdated fields")
+            .push()
+            .check_out('develop')
+            .new_branch('enhance/add_user')
+            .commit('allow externals to add users')
+            .push()
+            .new_branch('bugfix/add_user')
+            .commit('first round of fixes')
+            .push()
+            .new_branch('testing/add_user')
+            .commit('add test set for add_user feature')
+            .push()
+            .new_branch('chore/comments')
+            .commit('code maintenance')
+            .push()
+            .check_out('allow-ownership-link')
+        )
+
+        body: str = \
+            """
+            develop
+              enhance/feature
+                bugfix/feature
+                  allow-ownership-link
+                    restrict_access
+                      chore/redundant_checks
+            bugfix/add_user
+              testing/add_user
+                chore/comments
+            """
+        rewrite_branch_layout_file(body)
+
+        assert_success(
+            ['gitlab', 'update-mr-descriptions', '--mine'],
+            """
+            Checking for open GitLab MRs... OK
+            Description of MR !17 (restrict_access -> allow-ownership-link) has been updated
+            """
+        )
+
+        assert_success(
+            ['gitlab', 'update-mr-descriptions', '--related'],
+            """
+            Checking for open GitLab MRs... OK
+            Description of MR !18 (chore/redundant_checks -> restrict_access) has been updated
+            """
+        )
+        self.repo_sandbox.set_git_config_key("machete.gitlab.mrDescriptionIntroStyle", "full")
+        assert_success(
+            ['gitlab', 'update-mr-descriptions', '--related'],
+            """
+            Checking for open GitLab MRs... OK
+            Description of MR !6 (bugfix/feature -> enhance/feature) has been updated
+            Description of MR !12 (allow-ownership-link -> bugfix/feature) has been updated
+            Description of MR !17 (restrict_access -> allow-ownership-link) has been updated
+            """
+        )
+
+        assert_success(
+            ['gitlab', 'update-mr-descriptions', '--all'],
+            """
+            Checking for open GitLab MRs... OK
+            Description of MR !22 (testing/add_user -> bugfix/add_user) has been updated
+            Description of MR !24 (chore/comments -> testing/add_user) has been updated
+            """
+        )
+
+        mr = gitlab_api_state.get_mr_by_number(6)
+        assert mr is not None
+        assert mr['description'] == textwrap.dedent("""
+            <!-- start git-machete generated -->
+
+            ## Tree of downstream MRs as of 2023-12-31
+
+            * **MR !6 (THIS ONE)**:
+              `enhance/feature` ← `bugfix/feature`
+
+                * MR !12:
+                  `bugfix/feature` ← `allow-ownership-link`
+
+                  * MR !17:
+                    `allow-ownership-link` ← `restrict_access`
+
+                    * MR !18:
+                      `restrict_access` ← `chore/redundant_checks`
+
+            <!-- end git-machete generated -->
+
+            # Summary""")[1:]
+
+        mr = gitlab_api_state.get_mr_by_number(12)
+        assert mr is not None
+        assert mr['description'] == textwrap.dedent("""
+            <!-- start git-machete generated -->
+
+            # Based on MR !6
+
+            ## Chain of upstream MRs & tree of downstream MRs as of 2023-12-31
+
+            * MR !6:
+              `enhance/feature` ← `bugfix/feature`
+
+              * **MR !12 (THIS ONE)**:
+                `bugfix/feature` ← `allow-ownership-link`
+
+                  * MR !17:
+                    `allow-ownership-link` ← `restrict_access`
+
+                    * MR !18:
+                      `restrict_access` ← `chore/redundant_checks`
+
+            <!-- end git-machete generated -->
+
+            # Summary""")[1:]
+
+        mr = gitlab_api_state.get_mr_by_number(19)
+        assert mr is not None
+        assert mr['description'] == "# Summary"
+
+    def test_gitlab_update_mr_descriptions_misc_failures_and_warns(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitLabAPIState.with_mrs()))
+
+        self.patch_symbol(mocker, 'git_machete.gitlab.GitLabToken.for_domain', mock_gitlab_token_for_domain_none)
+        assert_success(
+            ["gitlab", "update-mr-descriptions", "--all"],
+            """
+            Checking for open GitLab MRs... OK
+            Warn: Currently there are no merge requests opened in project example-org/example-repo
+            """
+        )
+
+        assert_failure(
+            ["gitlab", "update-mr-descriptions", "--mine"],
+            """
+            Could not determine current user name, please check that the GitLab API token provided by one of the:
+            \t1. GITLAB_TOKEN environment variable
+            \t2. Content of the ~/.gitlab-token file
+            \t3. Current auth token from the glab GitLab CLI
+            is valid."""
+        )
+
+        self.patch_symbol(mocker, 'git_machete.gitlab.GitLabToken.for_domain', mock_gitlab_token_for_domain_fake)
+        assert_success(
+            ["gitlab", "update-mr-descriptions", "--mine"],
+            """
+            Checking for open GitLab MRs... OK
+            Warn: User gitlab_user has no open merge request in project example-org/example-repo
+            """
+        )
diff --git a/tests/test_traverse.py b/tests/test_traverse.py
index 0f984f8..16718cc 100644
--- a/tests/test_traverse.py
+++ b/tests/test_traverse.py
@@ -1167,8 +1167,8 @@ class TestTraverse(BaseTest):
         (
             self.repo_sandbox
             .remove_remote("origin")
-            .add_remote("origin_1", origin_1_remote_path)
-            .add_remote("origin_2", origin_2_remote_path)
+            .add_remote("origin-1", origin_1_remote_path)
+            .add_remote("origin-2", origin_2_remote_path)
         )
 
         self.repo_sandbox.new_branch("master").commit()
@@ -1176,11 +1176,14 @@ class TestTraverse(BaseTest):
 
         self.patch_symbol(mocker, 'builtins.input', mock_input_returning("xd"))
         assert_success(
-            ["traverse"],
+            ["traverse", "--fetch"],
             """
+            Fetching origin-1...
+            Fetching origin-2...
+
             Branch master is untracked and there's no origin remote.
-            [1] origin_1
-            [2] origin_2
+            [1] origin-1
+            [2] origin-2
             Select number 1..2 to specify the destination remote repository, or 'n' to skip this branch, or 'q' to quit the traverse:
 
               master * (untracked)
@@ -1190,18 +1193,21 @@ class TestTraverse(BaseTest):
         )
 
         self.patch_symbol(mocker, 'builtins.input', mock_input_returning("1", "o", "2", "yq"))
+        self.repo_sandbox.set_git_config_key("machete.traverse.fetch.origin-2", "false")
         assert_success(
-            ["traverse"],
+            ["traverse", "--fetch"],
             """
+            Fetching origin-1...
+
             Branch master is untracked and there's no origin remote.
-            [1] origin_1
-            [2] origin_2
+            [1] origin-1
+            [2] origin-2
             Select number 1..2 to specify the destination remote repository, or 'n' to skip this branch, or 'q' to quit the traverse:
-            Push untracked branch master to origin_1? (y, N, q, yq, o[ther-remote])
-            [1] origin_1
-            [2] origin_2
+            Push untracked branch master to origin-1? (y, N, q, yq, o[ther-remote])
+            [1] origin-1
+            [2] origin-2
             Select number 1..2 to specify the destination remote repository, or 'n' to skip this branch, or 'q' to quit the traverse:
-            Push untracked branch master to origin_2? (y, N, q, yq, o[ther-remote])
+            Push untracked branch master to origin-2? (y, N, q, yq, o[ther-remote])
             """
         )