Repository: VirtusLab/git-machete
Pull request #1340 (2024-09-24 13:34:08)

Add `-H`/`--sync-github-prs` and `-L`/`--sync-gitlab-mrs` flags to `traverse`
- [x] Update descriptions of PRs/MRs affected by each retarget, automatically 🤔 
- [x] Restack instead of plain retarget => future release
- [x] Error if multiple PRs have the current branch as head

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1340?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.42%. Comparing base [(`4a4dd28`)](https://app.codecov.io/gh/VirtusLab/git-machete/commit/4a4dd28c6b8a4f9e6409daa0a6f9db823ca1e3f2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) to head [(`1e40fe6`)](https://app.codecov.io/gh/VirtusLab/git-machete/commit/1e40fe6c7deb2553cde344a5c882eb6ca1fdd5ed?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           develop    #1340   +/-   ##
========================================
  Coverage    98.42%   98.42%           
========================================
  Files           14       14           
  Lines         3881     3881           
  Branches       849      849           
========================================
  Hits          3820     3820           
  Misses          39       39           
  Partials        22       22           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1340?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab).


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index d393e63..8df35a2 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,6 +1,8 @@
 # Release notes
 
-## New in git-machete 3.31.2
+## New in git-machete 3.32.0
+
+- added: flags `-H`/`--sync-github-prs` and `-L`/`--sync-gitlab-mrs` to `traverse` to suggest retargeting PRs/MRs when traversing (suggested by @chriscz)
 
 ## New in git-machete 3.31.1
 
diff --git a/completion/git-machete.completion.bash b/completion/git-machete.completion.bash
index 3f10474..7a9806a 100644
--- a/completion/git-machete.completion.bash
+++ b/completion/git-machete.completion.bash
@@ -17,7 +17,7 @@ _git_machete() {
   local common_opts="--debug -h --help -v --verbose"
   local add_opts="-f --as-first-child -o --onto= -R --as-root -y --yes"
   local advance_opts="-y --yes"
-  local anno_opts="-b --branch= -H -L --sync-github-prs --sync-gitlab-mrs"
+  local anno_opts="-b --branch= -H --sync-github-prs -L --sync-gitlab-mrs"
   local delete_unmanaged_opts="-y --yes"
   local diff_opts="-s --stat"
   local discover_opts="-C --checked-out-since= -l --list-commits -r --roots= -y --yes"
@@ -32,7 +32,7 @@ _git_machete() {
   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="
   local status_opts="--color= -L --list-commits-with-hashes -l --list-commits --no-detect-squash-merges"
-  local traverse_opts="-F --fetch -l --list-commits -M --merge -n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase --no-push --no-push-untracked --push --push-untracked --return-to= --start-from= -w --whole -W -y --yes"
+  local traverse_opts="-F --fetch -H --sync-github-prs -L --sync-gitlab-mrs -l --list-commits -M --merge -n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase --no-push --no-push-untracked --push --push-untracked --return-to= --start-from= -w --whole -W -y --yes"
   local update_opts="-f --fork-point= -M --merge -n --no-edit-merge --no-interactive-rebase"
 
   cur=${COMP_WORDS[$COMP_CWORD]}
diff --git a/completion/git-machete.completion.zsh b/completion/git-machete.completion.zsh
index 99b3ceb..854f1d2 100644
--- a/completion/git-machete.completion.zsh
+++ b/completion/git-machete.completion.zsh
@@ -140,6 +140,8 @@ _git-machete() {
         (t|traverse)
           _arguments \
             '(-F --fetch)'{-F,--fetch}'[Fetch the remotes of all managed branches at the beginning of traversal]' \
+            '(-H --sync-github-prs)'{-H,--sync-github-prs}'[Retarget GitHub PRs when their base branches are different than in machete file]' \
+            '(-L --sync-gitlab-mrs)'{-L,--sync-gitlab-mrs}'[Retarget GitLab MRs when their target branches are different than in machete file]' \
             '(-l --list-commits)'{-l,--list-commits}'[List the messages of commits introduced on each branch]' \
             '(-M --merge)'{-M,--merge}'[Update by merge rather than by rebase]' \
             '(-n)'-n'[If updating by rebase, equivalent to --no-interactive-rebase. If updating by merge, equivalent to --no-edit-merge]' \
diff --git a/completion/git-machete.fish b/completion/git-machete.fish
index 7cd803f..18f287b 100644
--- a/completion/git-machete.fish
+++ b/completion/git-machete.fish
@@ -213,22 +213,24 @@ complete -c git-machete -n "__fish_seen_subcommand_from status s; and not __fish
 
 # git machete traverse
 complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands" -f -a traverse -d 'Walk through the tree of branch dependencies and rebase, merge, slide out, push and/or pull each branch one by one'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --fetch -F"                -f -l fetch        -s F                             -d 'Fetch the remotes of all managed branches at the beginning of traversal (no git pull involved, only git fetch)'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --list-commits -l"         -f -l list-commits -s l                             -d 'When printing the status, additionally list the messages of commits introduced on each branch'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --merge -M"                -f -l merge        -s M                             -d 'Update by merge rather than by rebase'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --yes -y"                  -f                 -s n                             -d 'If updating by rebase, equivalent to --no-interactive-rebase. If updating by merge, equivalent to --no-edit-merge'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-detect-squash-merges" -f -l no-detect-squash-merges                       -d 'Only consider "strict" (fast-forward or 2-parent) merges, rather than rebase/squash merges, when detecting if a branch is merged into its upstream (parent)'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-edit-merge"           -f -l no-edit-merge                                 -d 'If updating by merge, skip opening the editor for merge commit message while doing git merge (i.e. pass --no-edit flag to underlying git merge). Not allowed if updating by rebase'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-interactive-rebase"   -f -l no-interactive-rebase                         -d 'If updating by rebase, run git rebase in non-interactive mode (without -i/--interactive flag). Not allowed if updating by merge'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-push"                 -f -l no-push                                       -d 'Do not push any (neither tracked nor untracked) branches to remote, re-enable via --push'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-push-untracked"       -f -l no-push-untracked                             -d 'Do not push untracked branches to remote, re-enable via --push-untracked'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --push"                    -f -l push                                          -d 'Push all (both tracked and untracked) branches to remote - default behavior'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --push-untracked"          -f -l push-untracked                                -d 'Push untracked branches to remote - default behavior'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --return-to"               -x -l return-to    -a 'stay here nearest-remaining' -d 'Specifies the branch to return after traversal is successfully completed'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --start-from"              -x -l start-from   -a 'here root first-root'        -d 'Specifies the branch to start the traversal from'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --whole -w"                -f -l whole        -s w                             -d 'Equivalent to -n --start-from=first-root --return-to=nearest-remaining'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from -W"                        -f                 -s W                             -d 'Equivalent to --fetch --whole; useful for even more automated traversal of all branches'
-complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --yes -y"                  -f -l yes          -s y                             -d 'Do not ask for any interactive input, including confirmation of rebase/push/pull. Implies -n'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --fetch -F"                -f -l fetch           -s F                             -d 'Fetch the remotes of all managed branches at the beginning of traversal (no git pull involved, only git fetch)'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --list-commits -l"         -f -l list-commits    -s l                             -d 'When printing the status, additionally list the messages of commits introduced on each branch'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --sync-gitlab-mrs"         -f -l sync-github-prs -s H                             -d 'Retarget GitHub PR when its base branch is different than in machete file'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --sync-github-prs"         -f -l sync-gitlab-mrs -s L                             -d 'Retarget GitLab MR when its target branch is different than in machete file'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --merge -M"                -f -l merge           -s M                             -d 'Update by merge rather than by rebase'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --yes -y"                  -f                    -s n                             -d 'If updating by rebase, equivalent to --no-interactive-rebase. If updating by merge, equivalent to --no-edit-merge'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-detect-squash-merges" -f -l no-detect-squash-merges                          -d 'Only consider "strict" (fast-forward or 2-parent) merges, rather than rebase/squash merges, when detecting if a branch is merged into its upstream (parent)'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-edit-merge"           -f -l no-edit-merge                                    -d 'If updating by merge, skip opening the editor for merge commit message while doing git merge (i.e. pass --no-edit flag to underlying git merge). Not allowed if updating by rebase'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-interactive-rebase"   -f -l no-interactive-rebase                            -d 'If updating by rebase, run git rebase in non-interactive mode (without -i/--interactive flag). Not allowed if updating by merge'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-push"                 -f -l no-push                                          -d 'Do not push any (neither tracked nor untracked) branches to remote, re-enable via --push'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-push-untracked"       -f -l no-push-untracked                                -d 'Do not push untracked branches to remote, re-enable via --push-untracked'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --push"                    -f -l push                                             -d 'Push all (both tracked and untracked) branches to remote - default behavior'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --push-untracked"          -f -l push-untracked                                   -d 'Push untracked branches to remote - default behavior'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --return-to"               -x -l return-to       -a 'stay here nearest-remaining' -d 'Specifies the branch to return after traversal is successfully completed'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --start-from"              -x -l start-from      -a 'here root first-root'        -d 'Specifies the branch to start the traversal from'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --whole -w"                -f -l whole           -s w                             -d 'Equivalent to -n --start-from=first-root --return-to=nearest-remaining'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from -W"                        -f                    -s W                             -d 'Equivalent to --fetch --whole; useful for even more automated traversal of all branches'
+complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --yes -y"                  -f -l yes             -s y                             -d 'Do not ask for any interactive input, including confirmation of rebase/push/pull. Implies -n'
 
 # git machete update
 complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands" -f -a update -d 'Sync the current branch with its upstream (parent) branch via rebase or merge'
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index c79fb82..4df2433 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" "Dec 17, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Jan 14, 2025" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.31.2
+git-machete \- git-machete 3.32.0
 .sp
 git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
 .sp
@@ -2032,6 +2032,7 @@ by setting \fBgit config machete.status.extraSpaceBeforeBranchName true\fP\&.
 git machete t[raverse] [\-F|\-\-fetch] [\-l|\-\-list\-commits] [\-M|\-\-merge]
                        [\-n|\-\-no\-edit\-merge|\-\-no\-interactive\-rebase] [\-\-[no\-]push] [\-\-[no\-]push\-untracked]
                        [\-\-return\-to=WHERE] [\-\-start\-from=WHERE] [\-\-squash\-merge\-detection=MODE]
+                       [\-H|\-\-sync\-github\-prs|\-L|\-\-sync\-gitlab\-mrs]
                        [\-w|\-\-whole] [\-W] [\-y|\-\-yes]
 .EE
 .UNINDENT
@@ -2078,6 +2079,14 @@ otherwise, if the branch is behind its remote counterpart:
 asks the user whether to \fBpull\fP the branch;
 .UNINDENT
 .IP \(bu 2
+if \fB\-H\fP/\fB\-\-sync\-github\-prs\fP or \fB\-L\fP/\fB\-\-sync\-gitlab\-mrs\fP option is present:
+.INDENT 2.0
+.IP \(bu 2
+asks the user whether to \fBretarget\fP the PR/MR if it exists for the given branch,
+and its base/target branch in GitHub/GitLab is different than the upstream in machete file
+(just as \fBgit machete github retarget\-pr\fP and \fBgit machete gitlab retarget\-mr\fP would do);
+.UNINDENT
+.IP \(bu 2
 and finally, if any of the above operations has been successfully completed:
 .INDENT 2.0
 .IP \(bu 2
@@ -2122,6 +2131,12 @@ when the current user is NOT the author of the PR/MR associated with that branch
 .B  \-F\fP,\fB  \-\-fetch
 Fetch the remotes of all managed branches at the beginning of traversal (no \fBgit pull\fP involved, only \fBgit fetch\fP).
 .TP
+.B  \-H\fP,\fB  \-\-sync\-github\-prs
+Retarget the PR if it exists for the given branch and its base branch in GitHub is different than the upstream in machete file
+.TP
+.B  \-L\fP,\fB  \-\-sync\-gitlab\-mrs
+Retarget the MR if it exists for the given branch and its target branch in GitLab is different than the upstream in machete file
+.TP
 .B  \-l\fP,\fB  \-\-list\-commits
 When printing the status, additionally list the messages of commits introduced on each branch.
 .TP
diff --git a/docs/source/cli/traverse.rst b/docs/source/cli/traverse.rst
index 9d350ff..c504ced 100644
--- a/docs/source/cli/traverse.rst
+++ b/docs/source/cli/traverse.rst
@@ -21,6 +21,7 @@ traverse
     git machete t[raverse] [-F|--fetch] [-l|--list-commits] [-M|--merge]
                            [-n|--no-edit-merge|--no-interactive-rebase] [--[no-]push] [--[no-]push-untracked]
                            [--return-to=WHERE] [--start-from=WHERE] [--squash-merge-detection=MODE]
+                           [-H|--sync-github-prs|-L|--sync-gitlab-mrs]
                            [-w|--whole] [-W] [-y|--yes]
 
 Traverses the branches in the order as they occur in branch layout file.
@@ -52,6 +53,12 @@ For each branch, the command:
 
   - asks the user whether to **pull** the branch;
 
+* if ``-H``/``--sync-github-prs`` or ``-L``/``--sync-gitlab-mrs`` option is present:
+
+  - asks the user whether to **retarget** the PR/MR if it exists for the given branch,
+    and its base/target branch in GitHub/GitLab is different than the upstream in machete file
+    (just as ``git machete github retarget-pr`` and ``git machete gitlab retarget-mr`` would do);
+
 * and finally, if any of the above operations has been successfully completed:
 
   - prints the updated ``status``.
@@ -88,6 +95,10 @@ when the current user is NOT the author of the PR/MR associated with that branch
 
 -F, --fetch                    Fetch the remotes of all managed branches at the beginning of traversal (no ``git pull`` involved, only ``git fetch``).
 
+-H, --sync-github-prs          Retarget the PR if it exists for the given branch and its base branch in GitHub is different than the upstream in machete file
+
+-L, --sync-gitlab-mrs          Retarget the MR if it exists for the given branch and its target branch in GitLab is different than the upstream in machete file
+
 -l, --list-commits             When printing the status, additionally list the messages of commits introduced on each branch.
 
 -M, --merge                    Update by merge rather than by rebase.
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 06bad3f..c657b28 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.31.2'
+__version__ = '3.32.0'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 4414a9e..2a63697 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -380,21 +380,23 @@ def create_cli_parser() -> argparse.ArgumentParser:
         add_help=False,
         parents=[common_args_parser])
     traverse_parser.add_argument('-F', '--fetch', action='store_true')
+    traverse_parser.add_argument('-H', '--sync-github-prs', action='store_true')
     traverse_parser.add_argument('-l', '--list-commits', action='store_true')
+    traverse_parser.add_argument('-L', '--sync-gitlab-mrs', action='store_true')
     traverse_parser.add_argument('-M', '--merge', action='store_true')
     traverse_parser.add_argument('-n', action='store_true')
+    traverse_parser.add_argument('--no-detect-squash-merges', action='store_true')
     traverse_parser.add_argument('--no-edit-merge', action='store_true')
     traverse_parser.add_argument('--no-interactive-rebase', action='store_true')
-    traverse_parser.add_argument('--no-detect-squash-merges', action='store_true')
-    traverse_parser.add_argument('--squash-merge-detection')
-    traverse_parser.add_argument('--push', action='store_true')
     traverse_parser.add_argument('--no-push', action='store_true')
-    traverse_parser.add_argument('--push-untracked', action='store_true')
     traverse_parser.add_argument('--no-push-untracked', action='store_true')
+    traverse_parser.add_argument('--push', action='store_true')
+    traverse_parser.add_argument('--push-untracked', action='store_true')
     traverse_parser.add_argument('--return-to')
+    traverse_parser.add_argument('--squash-merge-detection')
     traverse_parser.add_argument('--start-from')
-    traverse_parser.add_argument('-w', '--whole', action='store_true')
     traverse_parser.add_argument('-W', action='store_true')
+    traverse_parser.add_argument('-w', '--whole', action='store_true')
     traverse_parser.add_argument('-y', '--yes', action='store_true')
 
     update_parser = subparsers.add_parser(
@@ -960,6 +962,8 @@ def launch(orig_args: List[str]) -> None:
                 opt_return_to=opt_return_to,
                 opt_squash_merge_detection=opt_squash_merge_detection,
                 opt_start_from=opt_start_from,
+                opt_sync_github_prs=cli_opts.opt_sync_github_prs,
+                opt_sync_gitlab_mrs=cli_opts.opt_sync_gitlab_mrs,
                 opt_yes=cli_opts.opt_yes)
         elif cmd == "update":
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
diff --git a/git_machete/client.py b/git_machete/client.py
index e5375ff..5684d8d 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -27,6 +27,8 @@ from .git_operations import (HEAD, AnyBranchName, AnyRevision, BranchPair,
                              GitFormatPatterns, GitLogEntry,
                              LocalBranchShortName, RemoteBranchShortName,
                              SyncToRemoteStatus)
+from .github import GitHubClient
+from .gitlab import GitLabClient
 from .utils import (AnsiEscapeCodes, PopenResult, bold, colored, debug, dim,
                     excluding, flat_map, fmt, get_pretty_choices,
                     get_right_arrow, get_second, tupled, underline, warn)
@@ -780,6 +782,8 @@ class MacheteClient:
             opt_return_to: TraverseReturnTo,
             opt_squash_merge_detection: SquashMergeDetection,
             opt_start_from: TraverseStartFrom,
+            opt_sync_github_prs: bool,
+            opt_sync_gitlab_mrs: bool,
             opt_yes: bool
     ) -> None:
         self.expect_at_least_one_managed_branch()
@@ -795,6 +799,12 @@ class MacheteClient:
             if self.__git.get_remotes():
                 print("")
 
+        current_user: Optional[str] = None
+        if opt_sync_github_prs or opt_sync_gitlab_mrs:
+            spec = GitHubClient.spec() if opt_sync_github_prs else GitLabClient.spec()
+            self.__init_code_hosting_client(spec)
+            current_user = self.code_hosting_client.get_current_user_login()
+
         initial_branch = nearest_remaining_branch = self.__git.get_current_branch()
 
         if opt_start_from == TraverseStartFrom.ROOT:
@@ -839,6 +849,18 @@ class MacheteClient:
             else:
                 needs_remote_sync = False
 
+            if opt_sync_github_prs or opt_sync_gitlab_mrs:
+                prs = list(filter(lambda pr: pr.head == branch, self.get_all_open_prs()))
+                if len(prs) > 1:
+                    spec = self.code_hosting_client._spec
+                    raise MacheteException(
+                        f"Multiple {spec.pr_short_name}s have <b>{branch}</b> as its {spec.head_branch_name} branch: " +
+                        ", ".join(_pr.short_display_text() for _pr in prs))
+                pr = prs[0] if prs else None
+                needs_retarget_pr = pr and upstream and pr.base != upstream
+            else:
+                needs_retarget_pr = False
+
             use_merge = opt_merge or (branch in self.annotations and self.annotations[branch].qualifiers.update_with_merge)
 
             if needs_slide_out:
@@ -864,7 +886,7 @@ class MacheteClient:
                 if needs_parent_sync and branch in self.annotations:
                     needs_parent_sync = self.annotations[branch].qualifiers.rebase
 
-            if branch != current_branch and (needs_slide_out or needs_parent_sync or needs_remote_sync):
+            if branch != current_branch and (needs_slide_out or needs_parent_sync or needs_remote_sync or needs_retarget_pr):
                 self.__print_new_line(False)
                 print(f"Checking out {bold(branch)}")
                 self.__git.checkout(branch)
@@ -986,6 +1008,46 @@ class MacheteClient:
                 elif ans in ('q', 'quit'):
                     return
 
+            if needs_retarget_pr:
+                any_action_suggested = True
+                assert pr is not None
+                assert upstream is not None
+                spec = self.code_hosting_client._spec
+                self.__print_new_line(False)
+                ans_intro = f"Branch {bold(str(branch))} has a different {spec.pr_short_name} {spec.base_branch_name} ({bold(pr.base)}) " \
+                    f"in {spec.display_name} than in machete file ({bold(str(upstream))}).\n"
+                ans = self.ask_if(
+                    ans_intro + f"Retarget {pr.display_text()} to {bold(str(upstream))}?" + get_pretty_choices('y', 'N', 'q', 'yq'),
+                    ans_intro + f"Retargeting {pr.display_text()} to {bold(str(upstream))}...",
+                    opt_yes=opt_yes)
+                if ans in ('y', 'yes', 'yq'):
+                    self.code_hosting_client.set_base_of_pull_request(pr.number, base=upstream)
+                    print(f'{spec.base_branch_name.capitalize()} branch of {pr.display_text()} has been switched to {bold(str(upstream))}')
+                    pr.base = upstream
+
+                    anno = self.__annotations.get(branch)
+                    self.__annotations[branch] = Annotation(self.__pull_request_annotation(spec, pr, current_user),
+                                                            anno.qualifiers if anno else Qualifiers())
+                    self.save_branch_layout_file()
+
+                    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)
+                        print(f'Description of {pr.display_text()} has been updated')
+                        pr.description = new_description
+
+                    applicable_prs: List[PullRequest] = self.__get_applicable_pull_requests(related_to=pr)
+                    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()} '
+                                      f'(<b>{pr.head} {get_right_arrow()} {pr.base}</b>) has been updated'))
+
+                elif ans in ('q', 'quit'):
+                    return
+
             if needs_remote_sync:
                 any_action_suggested = True
                 try:
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index 8ae69fe..af7fa27 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -1436,6 +1436,7 @@ long_docs: Dict[str, str] = {
            git machete t[raverse] [-F|--fetch] [-l|--list-commits] [-M|--merge]
                                   [-n|--no-edit-merge|--no-interactive-rebase] [--[no-]push] [--[no-]push-untracked]
                                   [--return-to=WHERE] [--start-from=WHERE] [--squash-merge-detection=MODE]
+                                  [-H|--sync-github-prs|-L|--sync-gitlab-mrs]
                                   [-w|--whole] [-W] [-y|--yes]</b>
 
         Traverses the branches in the order as they occur in branch layout file.
@@ -1456,6 +1457,10 @@ long_docs: Dict[str, str] = {
               - asks the user whether to <b>reset</b> (`git reset --keep`) the branch to its remote counterpart
            * otherwise, if the branch is behind its remote counterpart:
               - asks the user whether to <b>pull</b> the branch;
+           * if `-H`/`--sync-github-prs` or `-L`/`--sync-gitlab-mrs` option is present:
+              - asks the user whether to <b>retarget</b> the PR/MR if it exists for the given branch,
+                and its base/target branch in GitHub/GitLab is different than the upstream in machete file
+                (just as `git machete github retarget-pr` and `git machete gitlab retarget-mr` would do);
            * and finally, if any of the above operations has been successfully completed:
               - prints the updated `status`.
 
@@ -1488,6 +1493,10 @@ long_docs: Dict[str, str] = {
         <b>Options:</b>
            <b>-F</b>, <b>--fetch</b>
               Fetch the remotes of all managed branches at the beginning of traversal (no `git pull` involved, only `git fetch`).
+           <b>-H</b>, <b>--sync-github-prs</b>
+              Retarget the PR if it exists for the given branch and its base branch in GitHub is different than the upstream in machete file
+           <b>-L</b>, <b>--sync-gitlab-mrs</b>
+              Retarget the MR if it exists for the given branch and its target branch in GitLab is different than the upstream in machete file
            <b>-l</b>, <b>--list-commits</b>
               When printing the status, additionally list the messages of commits introduced on each branch.
            <b>-M</b>, <b>--merge</b>


Test patch:
diff --git a/tests/completion_e2e/test_completion_e2e.py b/tests/completion_e2e/test_completion_e2e.py
index 9ba472f..27b66ba 100644
--- a/tests/completion_e2e/test_completion_e2e.py
+++ b/tests/completion_e2e/test_completion_e2e.py
@@ -129,11 +129,15 @@ test_cases: Dict[str, str] = {
     "git machete status --color=":
         "always auto never",
     "git machete t -":
-        "-F -M -W --debug --fetch -h --help -l --list-commits --merge -n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase "
-        "--no-push --no-push-untracked --push --push-untracked --return-to --start-from -v --verbose -w --whole -y --yes",
+        "-F -H -L -M -W --debug --fetch -h --help -l --list-commits --merge "
+        "-n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase "
+        "--no-push --no-push-untracked --push --push-untracked --return-to --start-from "
+        "--sync-github-prs --sync-gitlab-mrs -v --verbose -w --whole -y --yes",
     "git machete traverse -":
-        "-F -M -W --debug --fetch -h --help -l --list-commits --merge -n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase "
-        "--no-push --no-push-untracked --push --push-untracked --return-to --start-from -v --verbose -w --whole -y --yes",
+        "-F -H -L -M -W --debug --fetch -h --help -l --list-commits --merge "
+        "-n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase "
+        "--no-push --no-push-untracked --push --push-untracked --return-to --start-from "
+        "--sync-github-prs --sync-gitlab-mrs -v --verbose -w --whole -y --yes",
     "git machete update -":
         "-M --debug -f --fork-point -h --help --merge -n --no-edit-merge --no-interactive-rebase -v --verbose",
     "git machete update -f ":
diff --git a/tests/mockers.py b/tests/mockers.py
index ba4c193..5a40f19 100644
--- a/tests/mockers.py
+++ b/tests/mockers.py
@@ -58,19 +58,19 @@ def assert_success(cmd_and_args: Iterable[str], expected_result: str) -> None:
     assert actual_result == expected_result
 
 
-def assert_failure(cmd_and_args: Iterable[str], expected_result: str, expected_exception: Type[Exception] = MacheteException) -> None:
-    if expected_result.startswith("\n"):
+def assert_failure(cmd_and_args: Iterable[str], expected_message: str, expected_type: Type[Exception] = MacheteException) -> None:
+    if expected_message.startswith("\n"):
         # removeprefix is only available since Python 3.9
-        expected_result = expected_result[1:]
-    expected_result = textwrap.dedent(expected_result)
+        expected_message = expected_message[1:]
+    expected_message = textwrap.dedent(expected_message)
 
-    with pytest.raises(expected_exception) as e:
+    with pytest.raises(expected_type) as e:
         launch_command(*cmd_and_args)
     error_message = e.value.msg  # type: ignore[attr-defined]
     error_message = re.sub(" +$", "", error_message, flags=re.MULTILINE)
     if sys.platform == 'win32':
         error_message = error_message.replace('.git\\machete', '.git/machete')
-    assert error_message == expected_result
+    assert error_message == expected_message
 
 
 def read_branch_layout_file() -> str:
diff --git a/tests/test_advance.py b/tests/test_advance.py
index 946fa95..a979548 100644
--- a/tests/test_advance.py
+++ b/tests/test_advance.py
@@ -28,7 +28,7 @@ class TestAdvance(BaseTest):
         rewrite_branch_layout_file("master\n  develop")
         self.repo_sandbox.check_out("HEAD~")
 
-        assert_failure(["advance"], "Not currently on any branch", expected_exception=UnderlyingGitException)
+        assert_failure(["advance"], "Not currently on any branch", expected_type=UnderlyingGitException)
 
     def test_advance_for_no_applicable_downstream_branches(self) -> None:
         (
diff --git a/tests/test_file.py b/tests/test_file.py
index a1ea54e..479c3d2 100644
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -54,7 +54,7 @@ class TestFile(BaseTest):
 
     def test_file_outside_git_repo(self) -> None:
         os.chdir(mkdtemp())
-        assert_failure(["file", "--debug"], "Not a git repository", expected_exception=UnderlyingGitException)
+        assert_failure(["file", "--debug"], "Not a git repository", expected_type=UnderlyingGitException)
 
     def test_file_when_git_machete_is_a_directory(self) -> None:
         self.repo_sandbox.execute(f"mkdir .git{os.path.sep}machete")
diff --git a/tests/test_traverse.py b/tests/test_traverse.py
index 16718cc..dd43d35 100644
--- a/tests/test_traverse.py
+++ b/tests/test_traverse.py
@@ -66,26 +66,6 @@ class TestTraverse(BaseTest):
                     ignore-trailing
             """
         rewrite_branch_layout_file(body)
-        assert_success(
-            ["status"],
-            """
-            develop
-            |
-            x-allow-ownership-link (ahead of origin)
-            | |
-            | x-build-chain (untracked)
-            |
-            o-call-ws (ahead of origin)
-              |
-              x-drop-constraint (untracked)
-
-            master
-            |
-            o-hotfix/add-trigger (diverged from origin)
-              |
-              o-ignore-trailing * (diverged from & older than origin)
-            """
-        )
 
     def test_traverse_slide_out(self, mocker: MockerFixture) -> None:
         (
@@ -1081,7 +1061,7 @@ class TestTraverse(BaseTest):
         assert_failure(
             ["traverse", "--fetch", "-y", "--debug"],
             "Cannot perform git reset --keep origin/master. This is most likely caused by local uncommitted changes.",
-            expected_exception=UnderlyingGitException
+            expected_type=UnderlyingGitException
         )
 
     def test_traverse_with_stop_for_edit(self, mocker: MockerFixture) -> None:
diff --git a/tests/test_traverse_github.py b/tests/test_traverse_github.py
new file mode 100644
index 0000000..a0f1e9d
--- /dev/null
+++ b/tests/test_traverse_github.py
@@ -0,0 +1,229 @@
+import textwrap
+
+from pytest_mock import MockerFixture
+
+from .base_test import BaseTest
+from .mockers import (assert_failure, assert_success, mock_input_returning,
+                      rewrite_branch_layout_file)
+from .mockers_code_hosting import mock_from_url
+from .mockers_github import MockGitHubAPIState, mock_pr_json, mock_urlopen
+
+
+class TestTraverseGitHub(BaseTest):
+
+    @staticmethod
+    def github_api_state_for_test_traverse_sync_github_prs_multiple_same_head() -> MockGitHubAPIState:
+        return MockGitHubAPIState.with_prs(
+            mock_pr_json(head='build-chain', base='develop', number=1),
+            mock_pr_json(head='build-chain', base='allow-ownership-link', number=2),
+        )
+
+    def test_traverse_sync_github_prs_multiple_same_head(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(
+            self.github_api_state_for_test_traverse_sync_github_prs_multiple_same_head()))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop")
+            .commit()
+            .new_branch("allow-ownership-link")
+            .new_branch("build-chain")
+        )
+
+        body: str = \
+            """
+            develop
+                allow-ownership-link
+                    build-chain
+            """
+        rewrite_branch_layout_file(body)
+
+        assert_failure(
+            ["traverse", "--sync-github-prs"],
+            "Multiple PRs have build-chain as its head branch: #1, #2"
+        )
+
+    @staticmethod
+    def github_api_state_for_test_traverse_sync_github_prs() -> MockGitHubAPIState:
+        return MockGitHubAPIState.with_prs(
+            mock_pr_json(head='allow-ownership-link', base='develop', number=1),
+            mock_pr_json(head='build-chain', base='develop', number=2),
+            mock_pr_json(head='call-ws', base='build-chain', number=3),
+        )
+
+    def test_traverse_sync_github_prs(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        github_api_state = self.github_api_state_for_test_traverse_sync_github_prs()
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(github_api_state))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop")
+            .commit()
+            .push()
+            .new_branch("allow-ownership-link")
+            .commit()
+            .push()
+            .new_branch("build-chain")
+            .commit()
+            .push()
+            .new_branch("call-ws")
+            .commit()
+            .push()
+        )
+
+        body: str = \
+            """
+            develop
+                allow-ownership-link
+                    build-chain
+                        call-ws
+            """
+        rewrite_branch_layout_file(body)
+        self.repo_sandbox.check_out("build-chain")
+
+        self.patch_symbol(mocker, 'builtins.input', mock_input_returning("q"))
+        assert_success(
+            ["traverse", "--sync-github-prs"],
+            """
+            Checking for open GitHub PRs... OK
+            Branch build-chain has a different PR base (develop) in GitHub than in machete file (allow-ownership-link).
+            Retarget PR #2 to allow-ownership-link? (y, N, q, yq)
+            """
+        )
+
+        self.patch_symbol(mocker, 'builtins.input', mock_input_returning("n"))
+        assert_success(
+            ["traverse", "--sync-github-prs"],
+            """
+            Checking for open GitHub PRs... OK
+            Branch build-chain has a different PR base (develop) in GitHub than in machete file (allow-ownership-link).
+            Retarget PR #2 to allow-ownership-link? (y, N, q, yq)
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *
+                  |
+                  o-call-ws
+
+            No successor of build-chain needs to be slid out or synced with upstream branch or remote; nothing left to update
+            """
+        )
+
+        assert_success(
+            ["traverse", "--sync-github-prs", "-Wy"],
+            """
+            Fetching origin...
+
+            Checking out the first root branch (develop)
+            Checking for open GitHub PRs... OK
+
+            Checking out build-chain
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *
+                  |
+                  o-call-ws
+
+            Branch build-chain has a different PR base (develop) in GitHub than in machete file (allow-ownership-link).
+            Retargeting PR #2 to allow-ownership-link...
+            Base branch of PR #2 has been switched to allow-ownership-link
+            Description of PR #2 has been updated
+            Description of PR #3 (call-ws -> build-chain) has been updated
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *  PR #2 (some_other_user)
+                  |
+                  o-call-ws
+
+            No successor of build-chain needs to be slid out or synced with upstream branch or remote; nothing left to update
+            Returned to the initial branch build-chain
+            """)
+
+        pr2 = github_api_state.get_pull_by_number(2)
+        assert pr2 is not None
+        assert pr2['body'] == textwrap.dedent('''
+            <!-- start git-machete generated -->
+
+            # Based on PR #1
+
+            ## Chain of upstream PRs as of 2025-01-14
+
+            * PR #1:
+              `develop` ← `allow-ownership-link`
+
+              * **PR #2 (THIS ONE)**:
+                `allow-ownership-link` ← `build-chain`
+
+            <!-- end git-machete generated -->
+
+            # Summary''')[1:]
+
+        pr3 = github_api_state.get_pull_by_number(3)
+        assert pr3 is not None
+        assert pr3['body'] == textwrap.dedent('''
+            <!-- start git-machete generated -->
+
+            # Based on PR #2
+
+            ## Chain of upstream PRs as of 2025-01-14
+
+            * PR #1:
+              `develop` ← `allow-ownership-link`
+
+              * PR #2:
+                `allow-ownership-link` ← `build-chain`
+
+                * **PR #3 (THIS ONE)**:
+                  `build-chain` ← `call-ws`
+
+            <!-- end git-machete generated -->
+
+            # Summary''')[1:]
+
+        # Let's cover the case where the descriptions don't need to be updated after retargeting.
+
+        pr2['base']['ref'] = 'develop'
+        self.repo_sandbox.check_out("build-chain")
+        assert_success(
+            ["traverse", "-HWy"],
+            """
+            Fetching origin...
+
+            Checking out the first root branch (develop)
+            Checking for open GitHub PRs... OK
+
+            Checking out build-chain
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *  PR #2 (some_other_user)
+                  |
+                  o-call-ws
+
+            Branch build-chain has a different PR base (develop) in GitHub than in machete file (allow-ownership-link).
+            Retargeting PR #2 to allow-ownership-link...
+            Base branch of PR #2 has been switched to allow-ownership-link
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *  PR #2 (some_other_user)
+                  |
+                  o-call-ws
+
+            No successor of build-chain needs to be slid out or synced with upstream branch or remote; nothing left to update
+            Returned to the initial branch build-chain
+            """)
diff --git a/tests/test_traverse_gitlab.py b/tests/test_traverse_gitlab.py
new file mode 100644
index 0000000..582e314
--- /dev/null
+++ b/tests/test_traverse_gitlab.py
@@ -0,0 +1,229 @@
+import textwrap
+
+from pytest_mock import MockerFixture
+
+from .base_test import BaseTest
+from .mockers import (assert_failure, assert_success, mock_input_returning,
+                      rewrite_branch_layout_file)
+from .mockers_code_hosting import mock_from_url
+from .mockers_gitlab import MockGitLabAPIState, mock_mr_json, mock_urlopen
+
+
+class TestTraverseGitLab(BaseTest):
+
+    @staticmethod
+    def gitlab_api_state_for_test_traverse_sync_gitlab_mrs_multiple_same_source() -> MockGitLabAPIState:
+        return MockGitLabAPIState.with_mrs(
+            mock_mr_json(head='build-chain', base='develop', number=1),
+            mock_mr_json(head='build-chain', base='allow-ownership-link', number=2),
+        )
+
+    def test_traverse_sync_gitlab_mrs_multiple_same_source(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(
+            self.gitlab_api_state_for_test_traverse_sync_gitlab_mrs_multiple_same_source()))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop")
+            .commit()
+            .new_branch("allow-ownership-link")
+            .new_branch("build-chain")
+        )
+
+        body: str = \
+            """
+            develop
+                allow-ownership-link
+                    build-chain
+            """
+        rewrite_branch_layout_file(body)
+
+        assert_failure(
+            ["traverse", "--sync-gitlab-mrs"],
+            "Multiple MRs have build-chain as its source branch: !1, !2"
+        )
+
+    @staticmethod
+    def gitlab_api_state_for_test_traverse_sync_gitlab_mrs() -> MockGitLabAPIState:
+        return MockGitLabAPIState.with_mrs(
+            mock_mr_json(head='allow-ownership-link', base='develop', number=1),
+            mock_mr_json(head='build-chain', base='develop', number=2),
+            mock_mr_json(head='call-ws', base='build-chain', number=3),
+        )
+
+    def test_traverse_sync_gitlab_mrs(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        gitlab_api_state = self.gitlab_api_state_for_test_traverse_sync_gitlab_mrs()
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(gitlab_api_state))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop")
+            .commit()
+            .push()
+            .new_branch("allow-ownership-link")
+            .commit()
+            .push()
+            .new_branch("build-chain")
+            .commit()
+            .push()
+            .new_branch("call-ws")
+            .commit()
+            .push()
+        )
+
+        body: str = \
+            """
+            develop
+                allow-ownership-link
+                    build-chain
+                        call-ws
+            """
+        rewrite_branch_layout_file(body)
+        self.repo_sandbox.check_out("build-chain")
+
+        self.patch_symbol(mocker, 'builtins.input', mock_input_returning("q"))
+        assert_success(
+            ["traverse", "--sync-gitlab-mrs"],
+            """
+            Checking for open GitLab MRs... OK
+            Branch build-chain has a different MR target (develop) in GitLab than in machete file (allow-ownership-link).
+            Retarget MR !2 to allow-ownership-link? (y, N, q, yq)
+            """
+        )
+
+        self.patch_symbol(mocker, 'builtins.input', mock_input_returning("n"))
+        assert_success(
+            ["traverse", "--sync-gitlab-mrs"],
+            """
+            Checking for open GitLab MRs... OK
+            Branch build-chain has a different MR target (develop) in GitLab than in machete file (allow-ownership-link).
+            Retarget MR !2 to allow-ownership-link? (y, N, q, yq)
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *
+                  |
+                  o-call-ws
+
+            No successor of build-chain needs to be slid out or synced with upstream branch or remote; nothing left to update
+            """
+        )
+
+        assert_success(
+            ["traverse", "--sync-gitlab-mrs", "-Wy"],
+            """
+            Fetching origin...
+
+            Checking out the first root branch (develop)
+            Checking for open GitLab MRs... OK
+
+            Checking out build-chain
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *
+                  |
+                  o-call-ws
+
+            Branch build-chain has a different MR target (develop) in GitLab than in machete file (allow-ownership-link).
+            Retargeting MR !2 to allow-ownership-link...
+            Target branch of MR !2 has been switched to allow-ownership-link
+            Description of MR !2 has been updated
+            Description of MR !3 (call-ws -> build-chain) has been updated
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *  MR !2 (some_other_user)
+                  |
+                  o-call-ws
+
+            No successor of build-chain needs to be slid out or synced with upstream branch or remote; nothing left to update
+            Returned to the initial branch build-chain
+            """)
+
+        mr2 = gitlab_api_state.get_mr_by_number(2)
+        assert mr2 is not None
+        assert mr2['description'] == textwrap.dedent('''
+            <!-- start git-machete generated -->
+
+            # Based on MR !1
+
+            ## Chain of upstream MRs as of 2025-01-14
+
+            * MR !1:
+              `develop` ← `allow-ownership-link`
+
+              * **MR !2 (THIS ONE)**:
+                `allow-ownership-link` ← `build-chain`
+
+            <!-- end git-machete generated -->
+
+            # Summary''')[1:]
+
+        mr3 = gitlab_api_state.get_mr_by_number(3)
+        assert mr3 is not None
+        assert mr3['description'] == textwrap.dedent('''
+            <!-- start git-machete generated -->
+
+            # Based on MR !2
+
+            ## Chain of upstream MRs as of 2025-01-14
+
+            * MR !1:
+              `develop` ← `allow-ownership-link`
+
+              * MR !2:
+                `allow-ownership-link` ← `build-chain`
+
+                * **MR !3 (THIS ONE)**:
+                  `build-chain` ← `call-ws`
+
+            <!-- end git-machete generated -->
+
+            # Summary''')[1:]
+
+        # Let's cover the case where the descriptions don't need to be updated after retargeting.
+
+        mr2['target_branch'] = 'develop'
+        self.repo_sandbox.check_out("build-chain")
+        assert_success(
+            ["traverse", "-LWy"],
+            """
+            Fetching origin...
+
+            Checking out the first root branch (develop)
+            Checking for open GitLab MRs... OK
+
+            Checking out build-chain
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *  MR !2 (some_other_user)
+                  |
+                  o-call-ws
+
+            Branch build-chain has a different MR target (develop) in GitLab than in machete file (allow-ownership-link).
+            Retargeting MR !2 to allow-ownership-link...
+            Target branch of MR !2 has been switched to allow-ownership-link
+
+              develop
+              |
+              o-allow-ownership-link
+                |
+                o-build-chain *  MR !2 (some_other_user)
+                  |
+                  o-call-ws
+
+            No successor of build-chain needs to be slid out or synced with upstream branch or remote; nothing left to update
+            Returned to the initial branch build-chain
+            """)
diff --git a/tests/test_update.py b/tests/test_update.py
index 70fd76b..b17ffd2 100644
--- a/tests/test_update.py
+++ b/tests/test_update.py
@@ -108,7 +108,7 @@ class TestUpdate(BaseTest):
                 with fixed_author_and_committer_date_in_past():
                     expected_error_message = "git rebase --interactive --onto refs/heads/level-0-branch " \
                                              "c0306cdd500fc39869505592200258055407bcc6 level-1-branch returned 1"
-                    assert_failure(["update"], expected_error_message, expected_exception=UnderlyingGitException)
+                    assert_failure(["update"], expected_error_message, expected_type=UnderlyingGitException)
                 self.repo_sandbox.execute("git rebase --continue")
 
         new_fork_point_hash = launch_command("fork-point").strip()
@@ -341,7 +341,7 @@ class TestUpdate(BaseTest):
         assert_failure(["update"],
                        "git am session in progress. Conclude git am first "
                        "with git am --continue or git am --abort.",
-                       expected_exception=UnderlyingGitException)
+                       expected_type=UnderlyingGitException)
 
         self.repo_sandbox.execute("git am --abort")
 
@@ -352,7 +352,7 @@ class TestUpdate(BaseTest):
         assert_failure(["update"],
                        "Cherry pick in progress. Conclude the cherry pick first with "
                        "git cherry-pick --continue or git cherry-pick --abort.",
-                       expected_exception=UnderlyingGitException)
+                       expected_type=UnderlyingGitException)
 
         self.repo_sandbox.execute("git cherry-pick --abort")
 
@@ -363,7 +363,7 @@ class TestUpdate(BaseTest):
         assert_failure(["update"],
                        "Merge in progress. Conclude the merge first with "
                        "git merge --continue or git merge --abort.",
-                       expected_exception=UnderlyingGitException)
+                       expected_type=UnderlyingGitException)
 
         self.repo_sandbox.execute("git merge --abort")
 
@@ -375,7 +375,7 @@ class TestUpdate(BaseTest):
         assert_failure(["update"],
                        "Rebase of develop in progress. Conclude the rebase first with "
                        "git rebase --continue or git rebase --abort.",
-                       expected_exception=UnderlyingGitException)
+                       expected_type=UnderlyingGitException)
 
         self.repo_sandbox.execute("git rebase --abort")
 
@@ -386,6 +386,6 @@ class TestUpdate(BaseTest):
         assert_failure(["update"],
                        "Revert in progress. Conclude the revert first with "
                        "git revert --continue or git revert --abort.",
-                       expected_exception=UnderlyingGitException)
+                       expected_type=UnderlyingGitException)
 
         self.repo_sandbox.execute("git revert --abort")