Repository: VirtusLab/git-machete
Pull request #1383 (2025-01-14 18:32:38)

Release v3.32.0


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 510b17b..ebdc9f1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -4,14 +4,14 @@ executors:
   macos_executor:
     macos:
       # https://circleci.com/docs/using-macos/#supported-xcode-versions
-      xcode: 16.1.0
+      xcode: 16.2.0
     # https://circleci.com/docs/using-macos/#available-resource-classes
     # https://circleci.com/pricing/#comparison-table
     resource_class: macos.m1.medium.gen1
   ubuntu_executor:
     machine:
       # https://circleci.com/developer/images?imageType=machine
-      image: ubuntu-2204:2024.01.1
+      image: ubuntu-2404:2024.11.1
     environment:
       # Needed to avoid warning: https://superuser.com/questions/575844/warning-terminal-is-not-fully-functional
       TERM: ansi
@@ -19,7 +19,7 @@ executors:
   windows_executor:
     machine:
       # https://circleci.com/developer/images?imageType=machine
-      image: windows-server-2022-gui:2024.01.1
+      image: windows-server-2022-gui:2024.12.1
       shell: bash.exe
     resource_class: windows.large
 
@@ -238,11 +238,6 @@ jobs:
           # Side effects: potentially pushes a regenerated Docker image
           command: bash ci/aur/ci-run.sh --dry-run
 
-      - run:
-          name: '[Dry run] Build & upload deb package to PPA'
-          # Side effects: potentially pushes a regenerated Docker image
-          command: bash ci/deb-ppa-upload/ci-deploy.sh --dry-run
-
       - run:
           name: '[Dry run] Build Nix package'
           # Side effects: potentially pushes a regenerated Docker image
@@ -261,14 +256,6 @@ jobs:
           name: 'Publish to PyPI'
           command: bash ci/pypi/ci-deploy.sh
 
-  "build and upload deb package to ppa":
-    executor: ubuntu_executor
-    steps:
-      - checkout
-      - run:
-          name: 'Build & upload deb package to PPA'
-          command: bash ci/deb-ppa-upload/ci-deploy.sh
-
   "publish snap to the stable channel":
     executor: ubuntu_executor
     steps:
@@ -346,10 +333,6 @@ workflows:
           <<: *only_master
           requires:
             - dry run releases
-      - build and upload deb package to ppa:
-          <<: *only_master
-          requires:
-            - dry run releases
       - publish snap to the stable channel:
           <<: *only_master
           requires:
diff --git a/PACKAGES.md b/PACKAGES.md
index 5f55fe5..5439952 100644
--- a/PACKAGES.md
+++ b/PACKAGES.md
@@ -4,22 +4,22 @@ In case the given package system provides multiple channels (stable/edge etc.),
 the **edge-most** channel that corresponds to the **released** versions of git-machete is listed
 (so as to make spotting potential blockers/errors in the given system easier).
 
-| Package system                                      | Version                                                                                                         | Package link                                                                                                                                                                                                              | Definition link                                                                                                                            | PRs link                                                                     | Controlled by                                                                               | Released by                                                                                                                                                                           | Installs completions [^0] and/or man page? | Downloads, last modified etc.                                                                                                                                                                                                                                                                                         |
-|-----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Alpine Linux                                        | ![Alpine Linux Edge package](https://repology.org/badge/version-for-repo/alpine_edge/git-machete.svg?header=)   | [**Alpine Linux Edge package**](https://pkgs.alpinelinux.org/package/edge/community/x86/git-machete)                                                                                                                      | [APKBUILD](https://git.alpinelinux.org/aports/tree/community/git-machete/APKBUILD) (points to GitHub)                                      |                                                                              | Alpine Linux package maintainers                                                            | Alpine Linux package maintainers                                                                                                                                                      | bash, fish, zsh                            |                                                                                                                                                                                                                                                                                                                       |
-| Arch User Repository                                | ![AUR package](https://img.shields.io/aur/version/git-machete.svg?label=)                                       | [**AUR package**](https://aur.archlinux.org/packages/git-machete)                                                                                                                                                         | [PKGBUILD](https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=git-machete) (points to GitHub)                                          |                                                                              | AUR maintainers, incl. [Ila&iuml; Deutel](https://github.com/ilai-deutel)                   | AUR maintainers[^1]                                                                                                                                                                   | bash, fish, zsh + man page                 | ![AUR last modified](https://img.shields.io/aur/last-modified/git-machete.svg)                                                                                                                                                                                                                                        |
-| Conda Forge                                         | ![Conda package](https://img.shields.io/conda/vn/conda-forge/git-machete.svg?label=)                            | [**Conda package**](https://anaconda.org/conda-forge/git-machete)                                                                                                                                                         | [Recipe](https://github.com/conda-forge/git-machete-feedstock/blob/main/recipe/meta.yaml) (points to PyPI)                                 | [PRs](https://github.com/conda-forge/git-machete-feedstock/pulls?q=is%3Apr)  | Feedstock maintainers, incl. [@asford (Alex Ford)](https://github.com/asford) and ourselves | Feedstock GitHub bot                                                                                                                                                                  | -                                          | ![Conda downloads](https://img.shields.io/conda/dn/conda-forge/git-machete.svg)                                                                                                                                                                                                                                       |
-| Gentoo                                              | ![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/git-machete.svg?header=)                   | [**Gentoo package**](https://packages.gentoo.org/packages/dev-vcs/git-machete)                                                                                                                                            | [ebuild](https://gitweb.gentoo.org/repo/gentoo.git/tree/dev-vcs/git-machete) (points to GitHub)                                            |                                                                              | Gentoo package maintainers                                                                  | Gentoo package maintainers                                                                                                                                                            | bash, fish, zsh                            |                                                                                                                                                                                                                                                                                                                       |
-| GitHub Releases                                     | ![GitHub latest release](https://img.shields.io/github/v/release/VirtusLab/git-machete?label=)                  | [**GitHub releases**](https://github.com/VirtusLab/git-machete/releases)                                                                                                                                                  |                                                                                                                                            |                                                                              | Ourselves                                                                                   | Our CI on `master` builds                                                                                                                                                             | -                                          | ![GitHub releases total downloads](https://img.shields.io/github/downloads/VirtusLab/git-machete/total)[^2]                                                                                                                                                                                                           |
-| Homebrew                                            | ![homebrew formula](https://img.shields.io/homebrew/v/git-machete.svg?label=)                                   | [**Formula**](https://formulae.brew.sh/formula/git-machete)                                                                                                                                                               | [Formula source](https://github.com/Homebrew/homebrew-core/blob/master/Formula/g/git-machete.rb) (points to PyPI)                          | [PRs](https://github.com/Homebrew/homebrew-core/pulls?q=is%3Apr+git-machete) | Homebrew maintainers                                                                        | [BrewTestBot](https://github.com/BrewTestBot) [^3]                                                                                                                                    | bash, fish, zsh + man page                 | ![homebrew formula monthly downloads](https://img.shields.io/homebrew/installs/dm/git-machete.svg) <br/> ![homebrew formula quarterly downloads](https://img.shields.io/homebrew/installs/dq/git-machete.svg) <br/> ![homebrew formula yearly downloads](https://img.shields.io/homebrew/installs/dy/git-machete.svg) |
-| [LiGurOS (Gentoo fork)](https://liguros.gitlab.io/) | ![LiGurOS develop package](https://repology.org/badge/version-for-repo/liguros_develop/git-machete.svg?header=) |                                                                                                                                                                                                                           | [ebuild](https://gitlab.com/liguros/liguros-repo/-/tree/develop/dev-vcs/git-machete) (points to GitHub)                                    |                                                                              | LiGurOS package maintainers                                                                 | LiGurOS package maintainers                                                                                                                                                           | bash, fish, zsh                            |                                                                                                                                                                                                                                                                                                                       |
-| nixpkgs                                             | ![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/git-machete.svg?header=)   | [**nixpkgs unstable package**](https://search.nixos.org/packages?channel=unstable&show=git-machete&query=git-machete)                                                                                                     | [Expression](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/version-management/git-machete/default.nix) (points to GitHub) | [PRs](https://github.com/NixOS/nixpkgs/pulls?q=is%3Apr+git-machete)          | nixpkgs maintainers, incl. [@blitz (Julian Stecklina)](https://github.com/blitz)            | [@r-ryantm bot](https://github.com/r-ryantm) ([logs](https://r.ryantm.com/log/git-machete/))[^4]                                                                                      | bash, fish, zsh                            |                                                                                                                                                                                                                                                                                                                       |
-| PyPI                                                | ![PyPI package](https://img.shields.io/pypi/v/git-machete.svg?label=)                                           | [**PyPI package**](https://pypi.org/project/git-machete) <br/> [Test PyPI package](https://test.pypi.org/project/git-machete)                                                                                             |                                                                                                                                            |                                                                              | Ourselves                                                                                   | Our CI on `master` builds[^5]                                                                                                                                                         | -                                          | ![PyPI daily downloads](https://img.shields.io/pypi/dd/git-machete.svg) <br/> ![PyPI weekly downloads](https://img.shields.io/pypi/dw/git-machete.svg) <br/> ![PyPI monthly downloads](https://img.shields.io/pypi/dm/git-machete.svg) <br/> ![PyPI wheel?](https://img.shields.io/pypi/wheel/git-machete.svg)        |
-| PPA (Ubuntu)                                        | [^6]                                                                                                            | [**PPA packages**](https://launchpad.net/~virtuslab/+archive/ubuntu/git-machete/+packages)                                                                                                                                | [debian/ folder (in this repo)](debian)                                                                                                    |                                                                              | Ourselves                                                                                   | Our CI on `master` builds (uploads source package), then Launchpad build system (see [builds](https://launchpad.net/~virtuslab/+archive/ubuntu/git-machete/+builds?&build_state=all)) | -                                          |                                                                                                                                                                                                                                                                                                                       |
-| Read the Docs                                       | N/A                                                                                                             | [**Read the Docs (stable)**](https://git-machete.readthedocs.io/en/stable) <br/> [Read the Docs (latest)](https://git-machete.readthedocs.io/en/latest) <br/> [Admin page](https://readthedocs.org/projects/git-machete/) |                                                                                                                                            |                                                                              | Ourselves                                                                                   | Read the Docs itself (see [builds](https://readthedocs.org/projects/git-machete/builds)), triggered by GitHub webhook [^7]                                                            | N/A                                        | ![Read the Docs build status](https://readthedocs.org/projects/git-machete/badge/?version=stable)                                                                                                                                                                                                                     |
-| Scoop (Windows)                                     | ![Scoop](https://img.shields.io/scoop/v/git-machete?label=)                                                     | [**Scoop package**](https://scoop.sh/#/apps?q=git-machete)                                                                                                                                                                | [Manifest](https://github.com/ScoopInstaller/Main/blob/master/bucket/git-machete.json) (points to PyPI, auto-updates based on GitHub)      | [PRs](https://github.com/ScoopInstaller/Main/pulls?q=is%3Apr+git-machete)    | Scoop maintainers                                                                           | Scoop itself (thanks to `autoupdate` attribute in manifest)                                                                                                                           | -                                          |                                                                                                                                                                                                                                                                                                                       |
-| Snap                                                | ![Snap](https://img.shields.io/snapcraft/v/git-machete/latest/stable?label=)                                    | [**Snap package**](https://snapcraft.io/git-machete)                                                                                                                                                                      | [snapcraft.yaml (in this repo)](snap/snapcraft.yaml)                                                                                       |                                                                              | Ourselves                                                                                   | Our CI on `master` builds[^8]                                                                                                                                                         | -                                          |                                                                                                                                                                                                                                                                                                                       |
-| [T2 SDE](https://t2sde.org/)                        | ![T2 SDE package](https://repology.org/badge/version-for-repo/t2/git-machete.svg?header=)                       | [**T2 SDE package**](https://t2sde.org/packages/git-machete)                                                                                                                                                              | [Package definition](http://svn.exactcode.de/t2/trunk/package/contrib/git-machete/) (points to GitHub)                                     |                                                                              | T2 SDE package maintainers                                                                  | T2 SDE package maintainers                                                                                                                                                            | -                                          |                                                                                                                                                                                                                                                                                                                       |
+| Package system                                           | Version                                                                                                         | Package link                                                                                                                                                                                                              | Definition link                                                                                                                            | PRs link                                                                     | Controlled by                                                                               | Released by                                                                                                                                                                             | Installs completions [^0] and/or man page? | Downloads, last modified etc.                                                                                                                                                                                                                                                                                         |
+|----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Alpine Linux                                             | ![Alpine Linux Edge package](https://repology.org/badge/version-for-repo/alpine_edge/git-machete.svg?header=)   | [**Alpine Linux Edge package**](https://pkgs.alpinelinux.org/package/edge/community/x86/git-machete)                                                                                                                      | [APKBUILD](https://git.alpinelinux.org/aports/tree/community/git-machete/APKBUILD) (points to GitHub)                                      |                                                                              | Alpine Linux package maintainers                                                            | Alpine Linux package maintainers                                                                                                                                                        | bash, fish, zsh                            |                                                                                                                                                                                                                                                                                                                       |
+| Arch User Repository                                     | ![AUR package](https://img.shields.io/aur/version/git-machete.svg?label=)                                       | [**AUR package**](https://aur.archlinux.org/packages/git-machete)                                                                                                                                                         | [PKGBUILD](https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=git-machete) (points to GitHub)                                          |                                                                              | AUR maintainers, incl. [Ila&iuml; Deutel](https://github.com/ilai-deutel)                   | AUR maintainers[^1]                                                                                                                                                                     | bash, fish, zsh + man page                 | ![AUR last modified](https://img.shields.io/aur/last-modified/git-machete.svg)                                                                                                                                                                                                                                        |
+| Conda Forge                                              | ![Conda package](https://img.shields.io/conda/vn/conda-forge/git-machete.svg?label=)                            | [**Conda package**](https://anaconda.org/conda-forge/git-machete)                                                                                                                                                         | [Recipe](https://github.com/conda-forge/git-machete-feedstock/blob/main/recipe/meta.yaml) (points to PyPI)                                 | [PRs](https://github.com/conda-forge/git-machete-feedstock/pulls?q=is%3Apr)  | Feedstock maintainers, incl. [@asford (Alex Ford)](https://github.com/asford) and ourselves | Feedstock GitHub bot                                                                                                                                                                    | -                                          | ![Conda downloads](https://img.shields.io/conda/dn/conda-forge/git-machete.svg)                                                                                                                                                                                                                                       |
+| Gentoo                                                   | ![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/git-machete.svg?header=)                   | [**Gentoo package**](https://packages.gentoo.org/packages/dev-vcs/git-machete)                                                                                                                                            | [ebuild](https://gitweb.gentoo.org/repo/gentoo.git/tree/dev-vcs/git-machete) (points to GitHub)                                            |                                                                              | Gentoo package maintainers                                                                  | Gentoo package maintainers                                                                                                                                                              | bash, fish, zsh                            |                                                                                                                                                                                                                                                                                                                       |
+| GitHub Releases                                          | ![GitHub latest release](https://img.shields.io/github/v/release/VirtusLab/git-machete?label=)                  | [**GitHub releases**](https://github.com/VirtusLab/git-machete/releases)                                                                                                                                                  |                                                                                                                                            |                                                                              | Ourselves                                                                                   | Our CI on `master` builds                                                                                                                                                               | -                                          | ![GitHub releases total downloads](https://img.shields.io/github/downloads/VirtusLab/git-machete/total)[^2]                                                                                                                                                                                                           |
+| Homebrew                                                 | ![homebrew formula](https://img.shields.io/homebrew/v/git-machete.svg?label=)                                   | [**Formula**](https://formulae.brew.sh/formula/git-machete)                                                                                                                                                               | [Formula source](https://github.com/Homebrew/homebrew-core/blob/master/Formula/g/git-machete.rb) (points to PyPI)                          | [PRs](https://github.com/Homebrew/homebrew-core/pulls?q=is%3Apr+git-machete) | Homebrew maintainers                                                                        | [BrewTestBot](https://github.com/BrewTestBot) [^3]                                                                                                                                      | bash, fish, zsh + man page                 | ![homebrew formula monthly downloads](https://img.shields.io/homebrew/installs/dm/git-machete.svg) <br/> ![homebrew formula quarterly downloads](https://img.shields.io/homebrew/installs/dq/git-machete.svg) <br/> ![homebrew formula yearly downloads](https://img.shields.io/homebrew/installs/dy/git-machete.svg) |
+| [LiGurOS (Gentoo fork)](https://liguros.gitlab.io/)      | ![LiGurOS develop package](https://repology.org/badge/version-for-repo/liguros_develop/git-machete.svg?header=) |                                                                                                                                                                                                                           | [ebuild](https://gitlab.com/liguros/liguros-repo/-/tree/develop/dev-vcs/git-machete) (points to GitHub)                                    |                                                                              | LiGurOS package maintainers                                                                 | LiGurOS package maintainers                                                                                                                                                             | bash, fish, zsh                            |                                                                                                                                                                                                                                                                                                                       |
+| nixpkgs                                                  | ![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/git-machete.svg?header=)   | [**nixpkgs unstable package**](https://search.nixos.org/packages?channel=unstable&show=git-machete&query=git-machete)                                                                                                     | [Expression](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/version-management/git-machete/default.nix) (points to GitHub) | [PRs](https://github.com/NixOS/nixpkgs/pulls?q=is%3Apr+git-machete)          | nixpkgs maintainers, incl. [@blitz (Julian Stecklina)](https://github.com/blitz)            | [@r-ryantm bot](https://github.com/r-ryantm) ([logs](https://r.ryantm.com/log/git-machete/))[^4]                                                                                        | bash, fish, zsh                            |                                                                                                                                                                                                                                                                                                                       |
+| PyPI                                                     | ![PyPI package](https://img.shields.io/pypi/v/git-machete.svg?label=)                                           | [**PyPI package**](https://pypi.org/project/git-machete) <br/> [Test PyPI package](https://test.pypi.org/project/git-machete)                                                                                             |                                                                                                                                            |                                                                              | Ourselves                                                                                   | Our CI on `master` builds[^5]                                                                                                                                                           | -                                          | ![PyPI daily downloads](https://img.shields.io/pypi/dd/git-machete.svg) <br/> ![PyPI weekly downloads](https://img.shields.io/pypi/dw/git-machete.svg) <br/> ![PyPI monthly downloads](https://img.shields.io/pypi/dm/git-machete.svg) <br/> ![PyPI wheel?](https://img.shields.io/pypi/wheel/git-machete.svg)        |
+| _PPA (Ubuntu)_  **deprecated, no updates since v3.32.0** | [^6]                                                                                                            | [_PPA packages_](https://launchpad.net/~virtuslab/+archive/ubuntu/git-machete/+packages)                                                                                                                                  | [_debian/ folder (in this repo)_](https://github.com/VirtusLab/git-machete/tree/v3.31.1/debian)                                                                                                  |                                                                              | _Ourselves_                                                                                 | _Our CI on `master` builds (uploads source package), then Launchpad build system (see [builds](https://launchpad.net/~virtuslab/+archive/ubuntu/git-machete/+builds?&build_state=all))_ | -                                          |                                                                                                                                                                                                                                                                                                                       |
+| Read the Docs                                            | N/A                                                                                                             | [**Read the Docs (stable)**](https://git-machete.readthedocs.io/en/stable) <br/> [Read the Docs (latest)](https://git-machete.readthedocs.io/en/latest) <br/> [Admin page](https://readthedocs.org/projects/git-machete/) |                                                                                                                                            |                                                                              | Ourselves                                                                                   | Read the Docs itself (see [builds](https://readthedocs.org/projects/git-machete/builds)), triggered by GitHub webhook [^7]                                                              | N/A                                        | ![Read the Docs build status](https://readthedocs.org/projects/git-machete/badge/?version=stable)                                                                                                                                                                                                                     |
+| Scoop (Windows)                                          | ![Scoop](https://img.shields.io/scoop/v/git-machete?label=)                                                     | [**Scoop package**](https://scoop.sh/#/apps?q=git-machete)                                                                                                                                                                | [Manifest](https://github.com/ScoopInstaller/Main/blob/master/bucket/git-machete.json) (points to PyPI, auto-updates based on GitHub)      | [PRs](https://github.com/ScoopInstaller/Main/pulls?q=is%3Apr+git-machete)    | Scoop maintainers                                                                           | Scoop itself (thanks to `autoupdate` attribute in manifest)                                                                                                                             | -                                          |                                                                                                                                                                                                                                                                                                                       |
+| Snap                                                     | ![Snap](https://img.shields.io/snapcraft/v/git-machete/latest/stable?label=)                                    | [**Snap package**](https://snapcraft.io/git-machete)                                                                                                                                                                      | [snapcraft.yaml (in this repo)](snap/snapcraft.yaml)                                                                                       |                                                                              | Ourselves                                                                                   | Our CI on `master` builds[^8]                                                                                                                                                           | -                                          |                                                                                                                                                                                                                                                                                                                       |
+| [T2 SDE](https://t2sde.org/)                             | ![T2 SDE package](https://repology.org/badge/version-for-repo/t2/git-machete.svg?header=)                       | [**T2 SDE package**](https://t2sde.org/packages/git-machete)                                                                                                                                                              | [Package definition](http://svn.exactcode.de/t2/trunk/package/contrib/git-machete/) (points to GitHub)                                     |                                                                              | T2 SDE package maintainers                                                                  | T2 SDE package maintainers                                                                                                                                                              | -                                          |                                                                                                                                                                                                                                                                                                                       |
 
 [^0]: Completions are available under all package managers via `git machete completion <shell>`.
       This column tells whether the completions are installed out of the box in the system, with no need for `source <(git machete completion <shell>)` in shell rc file.
diff --git a/README.md b/README.md
index e8ccb77..1d0c919 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,6 @@
 [![PyPI package monthly downloads](https://img.shields.io/pypi/dm/git-machete.svg)](https://pypistats.org/packages/git-machete)
 [![Conda package](https://img.shields.io/conda/vn/conda-forge/git-machete.svg)](https://anaconda.org/conda-forge/git-machete)
 [![Conda downloads](https://img.shields.io/conda/dn/conda-forge/git-machete.svg)](https://anaconda.org/conda-forge/git-machete)
-[![Snap](https://snapcraft.io/git-machete/badge.svg)](https://snapcraft.io/git-machete)
 <br/>
 [![Read the Docs](https://readthedocs.org/projects/git-machete/badge/?version=latest)](https://git-machete.readthedocs.io/en/stable)
 [![License: MIT](https://img.shields.io/github/license/VirtusLab/git-machete)](https://github.com/VirtusLab/git-machete/blob/master/LICENSE)
@@ -95,16 +94,6 @@ It can also be installed via Ubuntu Software (simply search for `git-machete`).
 
 **Note:** classic confinement is necessary to ensure access to the editor installed in the system (to edit e.g. `.git/machete` file or rebase TODO list).
 
-### Using apt-get via PPA (Ubuntu)
-
-**Tip:** run `sudo apt-get install -y software-properties-common` first if `add-apt-repository` is not available on your system.
-
-```shell script
-sudo add-apt-repository ppa:virtuslab/git-machete
-sudo apt-get update
-sudo apt-get install -y python3-git-machete
-```
-
 ### Using Alpine, Arch, Gentoo & other Linux distro-specific package managers
 
 Check [Repology](https://repology.org/project/git-machete/versions) for the available distro-specific packages.
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index af8f148..e4ecdc2 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,10 @@
 # Release notes
 
+## 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)
+- removed: no longer release new packages to Ubuntu PPA
+
 ## New in git-machete 3.31.1
 
 - fixed: AUR package installation (reported by @jan-san)
diff --git a/ci/deb-ppa-upload/Dockerfile b/ci/deb-ppa-upload/Dockerfile
deleted file mode 100644
index f43dc10..0000000
--- a/ci/deb-ppa-upload/Dockerfile
+++ /dev/null
@@ -1,21 +0,0 @@
-FROM ubuntu:20.04
-SHELL ["/bin/bash", "-e", "-o", "pipefail", "-u", "-x", "-c"]
-
-# See https://github.com/phusion/baseimage-docker/issues/58 for why DEBIAN_FRONTEND=noninteractive is needed.
-RUN \
-    apt-get update; \
-    DEBIAN_FRONTEND=noninteractive apt-get install -y \
-      debhelper devscripts fakeroot dh-python gawk python3-all python3-paramiko python3-setuptools sudo; \
-    rm -rfv /var/lib/apt/lists/*
-
-COPY dput.cf.envsubst entrypoint.sh gpg-wrapper.sh release-notes-to-changelog.awk run.sh /
-RUN chmod a+r /dput.cf.envsubst /release-notes-to-changelog.awk
-RUN touch /etc/dput.cf
-RUN chmod a+w /etc/dput.cf
-RUN chmod a+rx /entrypoint.sh /gpg-wrapper.sh /run.sh
-ENTRYPOINT ["/entrypoint.sh"]
-CMD ["/run.sh"]
-# One extra level under / is needed, since Debian packaging tools will write files
-# in the parent directory of current working directory.
-WORKDIR /git-machete/mount/
-RUN chmod -R a+rwx /git-machete
diff --git a/ci/deb-ppa-upload/README.md b/ci/deb-ppa-upload/README.md
deleted file mode 100644
index a70a513..0000000
--- a/ci/deb-ppa-upload/README.md
+++ /dev/null
@@ -1,25 +0,0 @@
-## Build & uploading the package to PPA from the local machine
-
-```shell script
-cd ci/deb-ppa-upload/
-. export-gpg-ssh-config
-./local-run.sh
-```
-
-## Regenerate GPG keys
-
-```shell
-gpg --default-new-key-algo rsa4096 --gen-key
-# ECC doesn't seem to be fully supported by Launchpad as of early 2023,
-# see https://bugs.launchpad.net/launchpad/+bug/2002884.
-# Non-expiring, username `Pawel Lipski`, email `plipski@virtuslab.com`.
-# Specify a password, add the password to password manager and CI (GPG_PRIVATE_KEY_PASSPHRASE env var).
-
-gpg --armor --export-secret-key <key-id> | pbcopy
-# Add to password manager (directly) & CI (base64-encoded, GPG_PRIVATE_KEY_CONTENTS_BASE64 env var).
-
-gpg --send-keys --keyserver keyserver.ubuntu.com <key-id>
-# Check if the key has been uploaded correctly via https://keyserver.ubuntu.com/.
-
-# Then remove the old & add the new GPG key at https://launchpad.net/~virtuslab/+editpgpkeys, logging via admin@virtuslab.com account.
-```
diff --git a/ci/deb-ppa-upload/build-context/dput.cf.envsubst b/ci/deb-ppa-upload/build-context/dput.cf.envsubst
deleted file mode 100644
index de67541..0000000
--- a/ci/deb-ppa-upload/build-context/dput.cf.envsubst
+++ /dev/null
@@ -1,7 +0,0 @@
-
-; We need to use SFTP since FTP is insecure & completely blocked on Travis (not sure about Circle CI, but still)
-[ppa]
-fqdn = ppa.launchpad.net
-method = sftp
-incoming = ~$LAUNCHPAD_USERNAME/ubuntu/git-machete/
-login = $LAUNCHPAD_USERNAME
diff --git a/ci/deb-ppa-upload/build-context/entrypoint.sh b/ci/deb-ppa-upload/build-context/entrypoint.sh
deleted file mode 100644
index 1f15099..0000000
--- a/ci/deb-ppa-upload/build-context/entrypoint.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-set -e -o pipefail -u
-
-if [[ ${GID-} && ${UID-} ]]; then
-  if ! getent group "$GID" &>/dev/null; then
-    groupadd --gid="$GID" docker
-  fi
-  useradd --create-home --gid="$GID" --no-log-init --uid="$UID" docker
-
-  sudo --preserve-env --set-home --user=docker bash -c "$*"
-else
-  bash -c "$@"
-fi
diff --git a/ci/deb-ppa-upload/build-context/gpg-wrapper.sh b/ci/deb-ppa-upload/build-context/gpg-wrapper.sh
deleted file mode 100755
index 2ccd718..0000000
--- a/ci/deb-ppa-upload/build-context/gpg-wrapper.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-# --pinentry-mode=loopback is the only option (--batch, --yes, --passphrase-fd 0 all had no effect)
-# that makes it possible for gpg NOT to ask for password via a TTY within Docker context.
-gpg --pinentry-mode=loopback --passphrase-file="$HOME/.gnupg/passphrase.txt" "$@"
diff --git a/ci/deb-ppa-upload/build-context/release-notes-to-changelog.awk b/ci/deb-ppa-upload/build-context/release-notes-to-changelog.awk
deleted file mode 100644
index aaad6c1..0000000
--- a/ci/deb-ppa-upload/build-context/release-notes-to-changelog.awk
+++ /dev/null
@@ -1,23 +0,0 @@
-# Requires GNU awk (gawk) due to the use of |&
-
-function trailer(v) {
-  "git log -1 --date=rfc2822 --format=%cd v" v " 2>/dev/null || date --rfc-email" |& getline date
-  printf "\n -- %s <%s>  %s\n\n\n", gpg_username, gpg_email, date
-}
-
-/## New in git-machete .*/ {
-  if (version) {
-    trailer(version)
-  }
-  match($0, /[0-9.]+/)
-  newVersion = substr($0, RSTART, RLENGTH)
-  version = newVersion
-  print "python3-git-machete (" version "~" distro_number ") " distro_name "; urgency=medium\n"
-}
-
-/^- / {
-  gsub("^- ", "  * ")
-  print $0
-}
-
-END { trailer(version) }
diff --git a/ci/deb-ppa-upload/build-context/run.sh b/ci/deb-ppa-upload/build-context/run.sh
deleted file mode 100644
index d9afc85..0000000
--- a/ci/deb-ppa-upload/build-context/run.sh
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env bash
-
-set -e -o pipefail -u
-
-if ! ( [ -f setup.py ] && grep -q "name='git-machete'" setup.py ); then
-  echo "Error: the repository should be mounted as a volume under $(pwd)"
-  exit 1
-fi
-
-install -d -m 0700 ~/.gnupg/ ~/.gnupg/private-keys-v1.d/ ~/.ssh/
-echo "$GPG_PRIVATE_KEY_PASSPHRASE" > ~/.gnupg/passphrase.txt
-echo "$GPG_PRIVATE_KEY_CONTENTS_BASE64" | base64 -d > ~/.gnupg/private.key
-/gpg-wrapper.sh --import ~/.gnupg/private.key
-echo "$SSH_PRIVATE_KEY_CONTENTS_BASE64" | base64 -d > ~/.ssh/id_rsa
-chmod 400 ~/.ssh/id_rsa
-
-set -x
-VERSION=$(cut -d\' -f2 git_machete/__init__.py)
-export VERSION
-envsubst '$VERSION' < debian/files.envsubst > debian/files
-cp LICENSE debian/copyright
-# The first version ever released to PPA is 2.12.8, so we skip everything older than that from the changelog.
-sed '/## New in git-machete 2\.12\.7/,$d' RELEASE_NOTES.md | awk \
-  -v distro_name="$TARGET_DISTRO_NAME" \
-  -v distro_number="$TARGET_DISTRO_NUMBER" \
-  -v gpg_email="$GPG_EMAIL" \
-  -v gpg_username="$GPG_USERNAME" \
-  -f /release-notes-to-changelog.awk > debian/changelog
-
-# Since we upload over SFTP, we need to whitelist the host first to avoid the prompt.
-# Let's retry ssh-keyscan to protect again rare spurious failures.
-attempts=3
-i=1
-while true; do
-  if ssh-keyscan ppa.launchpad.net > ~/.ssh/known_hosts; then
-    break
-  elif (( i < attempts )); then
-    echo "Retrying ssh-keyscan..."
-    i=$((i + 1))
-    sleep 5
-  else
-    echo "ssh-keyscan did not succeed despite $attempts attempts"
-    exit 1
-  fi
-done
-
-# `-p` flag points to a script that wraps gpg so that we don't get asked for password to the private key on TTY.
-debuild -S -p"/gpg-wrapper.sh"
-cat ../python3-git-machete_*.dsc
-cat ../python3-git-machete_*_source.buildinfo
-cat ../python3-git-machete_*_source.changes
-
-tar_output=$(tar tvf ../python3-git-machete_*.tar.gz)
-echo "$tar_output"
-grep completion/ <<< "$tar_output"
-grep docs/man/git-machete\.1 <<< "$tar_output"
-
-envsubst '$LAUNCHPAD_USERNAME' < /dput.cf.envsubst | tee /etc/dput.cf
-if [[ $DO_DPUT == true ]]; then
-  dput ppa:virtuslab/git-machete ../python3-git-machete_*_source.changes
-else
-  echo "Refraining from running 'dput' since it's a dry run"
-fi
diff --git a/ci/deb-ppa-upload/ci-deploy.sh b/ci/deb-ppa-upload/ci-deploy.sh
deleted file mode 100644
index e0045db..0000000
--- a/ci/deb-ppa-upload/ci-deploy.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env bash
-
-set -e -o pipefail -u -x
-
-if [[ ${1-} == "--dry-run" || ${CIRCLE_BRANCH-} != "master" ]]; then
-  do_dput=false
-else
-  do_dput=true
-fi
-
-source "$(git rev-parse --show-toplevel)"/ci/ci-run-commons.sh
-
-docker_compose_pull_or_build_and_push deb-ppa-upload
-common_flags=(
-  -e "DO_DPUT=$do_dput"
-  -e "GPG_EMAIL=plipski@virtuslab.com"
-  -e "GPG_USERNAME=Pawel Lipski"
-  -e "LAUNCHPAD_USERNAME=virtuslab")
-docker-compose --ansi=never run -e TARGET_DISTRO_NAME=bionic -e TARGET_DISTRO_NUMBER=18.04 "${common_flags[@]}" deb-ppa-upload
-docker-compose --ansi=never run -e TARGET_DISTRO_NAME=focal  -e TARGET_DISTRO_NUMBER=20.04 "${common_flags[@]}" deb-ppa-upload
-docker-compose --ansi=never run -e TARGET_DISTRO_NAME=jammy  -e TARGET_DISTRO_NUMBER=22.04 "${common_flags[@]}" deb-ppa-upload
diff --git a/ci/deb-ppa-upload/docker-compose.yml b/ci/deb-ppa-upload/docker-compose.yml
deleted file mode 100644
index 05a3f47..0000000
--- a/ci/deb-ppa-upload/docker-compose.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-services:
-  deb-ppa-upload:
-    image: gitmachete/ci-deb-ppa-upload:${DIRECTORY_HASH:-unspecified}
-    platform: linux/amd64
-    build:
-      context: build-context
-      dockerfile: ../Dockerfile # relative to build-context
-    volumes:
-      # Host path is relative to current directory, not build-context
-      - ./../..:/git-machete/mount/
-    environment:
-      - DO_DPUT
-      - GPG_EMAIL
-      - GPG_USERNAME
-      - LAUNCHPAD_USERNAME
-      - TARGET_DISTRO_NAME
-      - TARGET_DISTRO_NUMBER
-    env_file:
-      - gpg-ssh.env
diff --git a/ci/deb-ppa-upload/export-gpg-ssh-config b/ci/deb-ppa-upload/export-gpg-ssh-config
deleted file mode 100644
index b74ff27..0000000
--- a/ci/deb-ppa-upload/export-gpg-ssh-config
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-# shellcheck disable=SC2155
-
-if [[ $OSTYPE == darwin* ]]; then
-  wrap0_flag=""
-else
-  wrap0_flag="--wrap=0"
-fi
-
-export GPG_PRIVATE_KEY_PASSPHRASE=$(cat ~/.gnupg/passphrase.txt)
-export GPG_PRIVATE_KEY_CONTENTS_BASE64=$(base64 $wrap0_flag < ~/.gnupg/private.key)
-export SSH_PRIVATE_KEY_CONTENTS_BASE64=$(base64 $wrap0_flag < ~/.ssh/id_rsa)
diff --git a/ci/deb-ppa-upload/gpg-ssh.env b/ci/deb-ppa-upload/gpg-ssh.env
deleted file mode 100644
index d865ca8..0000000
--- a/ci/deb-ppa-upload/gpg-ssh.env
+++ /dev/null
@@ -1,3 +0,0 @@
-GPG_PRIVATE_KEY_PASSPHRASE
-GPG_PRIVATE_KEY_CONTENTS_BASE64
-SSH_PRIVATE_KEY_CONTENTS_BASE64
diff --git a/ci/deb-ppa-upload/local-run.sh b/ci/deb-ppa-upload/local-run.sh
deleted file mode 100755
index 02a35b3..0000000
--- a/ci/deb-ppa-upload/local-run.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-
-set -e -o pipefail -u
-
-source "$(git rev-parse --show-toplevel)"/ci/local-run-commons.sh deb-ppa-upload
-
-export_directory_hash deb-ppa-upload
-cd "$(git rev-parse --show-toplevel)"/ci/deb-ppa-upload/
-check_env gpg-ssh.env
-
-set -x
-docker-compose --progress=plain build deb-ppa-upload
-common_flags=(
-  -e "GID=$(id -g)"
-  -e "UID=$(id -u)"
-  -e "DO_DPUT=true"
-  -e "GPG_EMAIL=pawel.p.lipski@gmail.com"
-  -e "GPG_USERNAME=pawel-lipski-launchpad"
-  -e "LAUNCHPAD_USERNAME=pawel-lipski")
-docker-compose run -e TARGET_DISTRO_NAME=bionic -e TARGET_DISTRO_NUMBER=18.04 "${common_flags[@]}" deb-ppa-upload
-docker-compose run -e TARGET_DISTRO_NAME=focal  -e TARGET_DISTRO_NUMBER=20.04 "${common_flags[@]}" deb-ppa-upload
-docker-compose run -e TARGET_DISTRO_NAME=jammy  -e TARGET_DISTRO_NUMBER=22.04 "${common_flags[@]}" deb-ppa-upload
diff --git a/ci/tox/build-context/requirements.txt b/ci/tox/build-context/requirements.txt
index 3fa31c0..cfeaf20 100644
--- a/ci/tox/build-context/requirements.txt
+++ b/ci/tox/build-context/requirements.txt
@@ -5,7 +5,7 @@ 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.3.0; python_version >= '3.8' and python_version < '3.9'
-setuptools==75.6.0; python_version >= '3.9'
+setuptools==75.8.0; python_version >= '3.9'
 
 tox==3.28.0; python_version <= '3.6'
 tox==4.8.0; python_version >= '3.7' and python_version < '3.8'
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/debian/.gitignore b/debian/.gitignore
deleted file mode 100644
index 77af607..0000000
--- a/debian/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-# These files are automatically generated by ci/deb-ppa-upload/build-context/run.sh:
-# * debian/changelog from RELEASE_NOTES.md
-# * debian/copyright from LICENSE
-# * debian/files from debian/files.envsubst
-changelog
-copyright
-files
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index f599e28..0000000
--- a/debian/compat
+++ /dev/null
@@ -1,1 +0,0 @@
-10
diff --git a/debian/control b/debian/control
deleted file mode 100644
index cdaa196..0000000
--- a/debian/control
+++ /dev/null
@@ -1,12 +0,0 @@
-Source: python3-git-machete
-Maintainer: Pawel Lipski <plipski@virtuslab.com>
-Build-Depends: debhelper,dh-python,git,python3-all,python3-setuptools
-Section: devel
-Priority: optional
-Standards-Version: 4.1.4
-X-Python3-Version: >= 3.6
-
-Package: python3-git-machete
-Architecture: any
-Description: Probably the sharpest git repository organizer & rebase/merge workflow automation tool you've ever seen
-Depends: ${python3:Depends}
diff --git a/debian/files.envsubst b/debian/files.envsubst
deleted file mode 100644
index d876e6c..0000000
--- a/debian/files.envsubst
+++ /dev/null
@@ -1,1 +0,0 @@
-python3-git-machete_$VERSION_source.buildinfo devel optional
diff --git a/debian/rules b/debian/rules
deleted file mode 100755
index 05a3d59..0000000
--- a/debian/rules
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/make -f
-
-#export DH_VERBOSE = 1
-export PYBUILD_NAME = git-machete
-
-%:
-	dh $@ --with python3 --buildsystem=pybuild
diff --git a/debian/source/options b/debian/source/options
deleted file mode 100644
index 59986e1..0000000
--- a/debian/source/options
+++ /dev/null
@@ -1,43 +0,0 @@
-tar-ignore = AUTHORS
-tar-ignore = BADGES.md
-tar-ignore = blogs
-tar-ignore = build
-tar-ignore = ChangeLog
-tar-ignore = ci
-tar-ignore = .circleci
-tar-ignore = CONTRIBUTING.md
-tar-ignore = .coverage
-tar-ignore = dist
-tar-ignore = .dockerignore
-tar-ignore = docs/*.py
-tar-ignore = docs/*.sh
-tar-ignore = docs/source
-tar-ignore = *.egg-info
-tar-ignore = .eggs
-tar-ignore = *.envsubst
-tar-ignore = graphics
-tar-ignore = .git
-tar-ignore = .github
-tar-ignore = .gitignore
-tar-ignore = hook_samples
-tar-ignore = htmlcov
-tar-ignore = .idea
-tar-ignore = ISSUE_TEMPLATE.md
-tar-ignore = LICENSE
-tar-ignore = Makefile
-tar-ignore = mypy.ini
-tar-ignore = .mypy_cache
-tar-ignore = PACKAGES.md
-tar-ignore = __pycache__
-tar-ignore = *.pyc
-tar-ignore = .python-version
-tar-ignore = .readthedocs.yaml
-tar-ignore = requirements
-tar-ignore = .remarkrc*.yml
-tar-ignore = RELEASE_NOTES.md
-tar-ignore = snap
-tar-ignore = *.swp
-tar-ignore = test-results
-tar-ignore = tests
-tar-ignore = .tox
-tar-ignore = tox.ini
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index a749e1c..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" "Nov 08, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Jan 14, 2025" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.31.1
+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 2702903..c657b28 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.31.1'
+__version__ = '3.32.0'
diff --git a/git_machete/annotation.py b/git_machete/annotation.py
index 07841fd..2ee14e3 100644
--- a/git_machete/annotation.py
+++ b/git_machete/annotation.py
@@ -1,87 +1,73 @@
 import re
+from typing import Callable, NamedTuple
 
 from git_machete import utils
 
 
-class Qualifiers:
-    def __init__(self, annotation: str):
-        self.__annotation_without_qualifiers: str = annotation
-        self.__push_text: str = ''
-        self.__rebase_text: str = ''
-        self.__slide_out_text: str = ''
-        self.__update_with_merge_text: str = ""
-        self.rebase: bool = True
-        self.push: bool = True
-        self.slide_out: bool = True
-        self.update_with_merge: bool = False
+class Qualifiers(NamedTuple):
+    rebase: bool = True
+    push: bool = True
+    slide_out: bool = True
+    update_with_merge: bool = False
 
-        def match_pattern(pattern: str) -> str:
-            return f".*\\b{pattern}\\b.*"
+    def is_default(self) -> bool:
+        return self.rebase and self.push and self.slide_out and not self.update_with_merge
 
-        def sub_pattern(pattern: str) -> str:
-            return f"[ ]?{pattern}[ ]?"
+    def is_non_default(self) -> bool:
+        return not self.is_default()
 
-        rebase_anno = 'rebase=no'
-        rebase_match = re.match(match_pattern(rebase_anno), annotation)
-        if rebase_match:
-            self.rebase = False
-            self.__rebase_text = rebase_anno
-            self.__annotation_without_qualifiers = re.sub(sub_pattern(rebase_anno), ' ', self.__annotation_without_qualifiers)
+    def __str__(self) -> str:
+        segments = ["rebase=no" if not self.rebase else None,
+                    "push=no" if not self.push else None,
+                    "slide-out=no" if not self.slide_out else None,
+                    "update=merge" if self.update_with_merge else None]
+        return ' '.join(filter(None, segments))
 
-        push_anno = 'push=no'
-        push_match = re.match(match_pattern(push_anno), annotation)
-        if push_match:
-            self.push = False
-            self.__push_text = push_anno
-            self.__annotation_without_qualifiers = re.sub(sub_pattern(push_anno), ' ', self.__annotation_without_qualifiers)
 
-        slide_out_anno = 'slide-out=no'
-        slide_out_match = re.match(match_pattern(slide_out_anno), annotation)
-        if slide_out_match:
-            self.slide_out = False
-            self.__slide_out_text = slide_out_anno
-            self.__annotation_without_qualifiers = re.sub(sub_pattern(slide_out_anno), ' ', self.__annotation_without_qualifiers)
+class Annotation(NamedTuple):
+    text_without_qualifiers: str
+    qualifiers: Qualifiers
 
-        update_with_merge_anno = 'update=merge'
-        update_with_merge_match = re.match(match_pattern(update_with_merge_anno), annotation)
-        if update_with_merge_match:
-            self.update_with_merge = True
-            self.__update_with_merge_text = update_with_merge_anno
-            self.__annotation_without_qualifiers = re.sub(sub_pattern(update_with_merge_anno), ' ', self.__annotation_without_qualifiers)
-
-    def get_annotation_text_without_qualifiers(self) -> str:
-        return self.__annotation_without_qualifiers.strip()
-
-    def get_qualifiers_text(self) -> str:
-        text = f'{self.__rebase_text} {self.__push_text} {self.__slide_out_text} {self.__update_with_merge_text}'
-        return re.sub(' +', ' ', text).strip()
-
-
-class Annotation:
-    def __init__(self, text: str):
-        self.text = text.strip()
-        self.qualifiers = Qualifiers(text)
-        self.text_without_qualifiers = self.qualifiers.get_annotation_text_without_qualifiers()
-        self.qualifiers_text = self.qualifiers.get_qualifiers_text()
-
-    def get_unformatted_text(self) -> str:
-        if not (self.text_without_qualifiers or self.qualifiers_text):
+    @property
+    def unformatted_full_text(self) -> str:
+        if not (self.text_without_qualifiers or self.qualifiers.is_non_default()):
             return ''
-        annotation_text = ' '
+        result = ''
         if self.text_without_qualifiers:
-            annotation_text += self.text_without_qualifiers
-        if self.qualifiers_text:
-            annotation_text += ' ' if self.text_without_qualifiers else ''
-            annotation_text += self.qualifiers_text
-        return annotation_text
+            result += self.text_without_qualifiers
+        if self.text_without_qualifiers and self.qualifiers.is_non_default():
+            result += ' '
+        if self.qualifiers.is_non_default():
+            result += str(self.qualifiers)
+        return result
 
-    def get_formatted_text(self) -> str:
-        if not (self.text_without_qualifiers or self.qualifiers_text):
+    @property
+    def formatted_full_text(self) -> str:
+        if not (self.text_without_qualifiers or self.qualifiers.is_non_default()):
             return ''
-        annotation_text = "  "
+        result = ''
         if self.text_without_qualifiers:
-            annotation_text += utils.dim(self.text_without_qualifiers)
-        if self.qualifiers_text:
-            annotation_text += ' ' if self.text_without_qualifiers else ''
-            annotation_text += utils.dim(utils.underline(s=self.qualifiers_text))
-        return annotation_text
+            result += utils.dim(self.text_without_qualifiers)
+        if self.text_without_qualifiers and self.qualifiers.is_non_default():
+            result += ' '
+        if self.qualifiers.is_non_default():
+            result += utils.dim(utils.underline(s=str(self.qualifiers)))
+        return result
+
+    @staticmethod
+    def parse(text_with_qualifiers: str) -> "Annotation":
+        text_without_qualifiers = text_with_qualifiers
+        qualifiers = Qualifiers()
+
+        def parse_one(pattern: str, qf: Callable[[Qualifiers], Qualifiers]) -> None:
+            nonlocal qualifiers, text_without_qualifiers
+            match = re.match(f".*\\b{pattern}\\b.*", text_without_qualifiers)
+            if match:
+                qualifiers = qf(qualifiers)
+                text_without_qualifiers = re.sub(f"[ ]?{pattern}[ ]?", ' ', text_without_qualifiers)
+
+        parse_one('rebase=no', lambda q: q._replace(rebase=False))
+        parse_one('push=no', lambda q: q._replace(push=False))
+        parse_one('slide-out=no', lambda q: q._replace(slide_out=False))
+        parse_one('update=merge', lambda q: q._replace(update_with_merge=True))
+        return Annotation(text_without_qualifiers.strip(), qualifiers)
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 b971c95..5684d8d 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -12,7 +12,7 @@ from typing import (Callable, Dict, Iterator, List, Optional, Set, Tuple, Type,
                     TypeVar)
 
 from . import git_config_keys, utils
-from .annotation import Annotation
+from .annotation import Annotation, Qualifiers
 from .code_hosting import (CodeHostingClient, CodeHostingSpec,
                            OrganizationAndRepository,
                            OrganizationAndRepositoryAndRemote, PullRequest,
@@ -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)
@@ -199,7 +201,7 @@ class MacheteClient:
                                                                        line.strip().split(" ", 1)]
             branch = branch_and_maybe_annotation[0]
             if len(branch_and_maybe_annotation) > 1:
-                self.__annotations[branch] = Annotation(branch_and_maybe_annotation[1])
+                self.__annotations[branch] = Annotation.parse(branch_and_maybe_annotation[1])
             if branch in self.managed_branches:
                 raise MacheteException(
                     f"{self.__branch_layout_file_path}, line {index + 1}: branch "
@@ -298,7 +300,7 @@ class MacheteClient:
             self.__indent = "  "
 
         def render_dfs(branch: LocalBranchShortName, depth: int) -> List[str]:
-            annotation = self.__annotations[branch].get_unformatted_text() if branch in self.__annotations else ""
+            annotation = (" " + self.__annotations[branch].unformatted_full_text) if branch in self.__annotations else ""
             assert self.__indent is not None
             res: List[str] = [depth * self.__indent + branch + annotation]
             for down_branch in self.__down_branches.get(branch) or []:
@@ -424,12 +426,12 @@ class MacheteClient:
         if branch in self.__annotations and words == ['']:
             del self.__annotations[branch]
         else:
-            self.__annotations[branch] = Annotation(" ".join(words))
+            self.__annotations[branch] = Annotation.parse(" ".join(words))
         self.save_branch_layout_file()
 
     def print_annotation(self, branch: LocalBranchShortName) -> None:
         if branch in self.__annotations:
-            print(self.__annotations[branch].text)
+            print(self.__annotations[branch].text_without_qualifiers)
 
     def update(
             self, *, opt_merge: bool, opt_no_edit_merge: bool,
@@ -486,7 +488,7 @@ class MacheteClient:
         self.__up_branch = {}
         self.__indent = "  "
         for branch in self.annotations.keys():
-            self.annotations[branch].text_without_qualifiers = ''
+            self.annotations[branch] = self.annotations[branch]._replace(text_without_qualifiers='')
 
         root_of = dict((branch, branch) for branch in all_local_branches)
 
@@ -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:
@@ -1201,8 +1263,8 @@ class MacheteClient:
                 current = bold(branch)
 
             anno: str = ''
-            if branch in self.__annotations:
-                anno = self.__annotations[branch].get_formatted_text()
+            if branch in self.__annotations and self.__annotations[branch].formatted_full_text:
+                anno = '  ' + self.__annotations[branch].formatted_full_text
 
             s, remote = self.__git.get_combined_remote_sync_status(branch)
             sync_status = {
@@ -1761,20 +1823,21 @@ class MacheteClient:
                          f'than in machete file ({bold(upstream) if upstream else "<none, is a root>"})')
                     anno += (f" WRONG {spec.pr_short_name} {spec.base_branch_name.upper()} or MACHETE PARENT? "
                              f"{spec.pr_short_name} has {pr.base}")
-                old_annotation_text, old_annotation_qualifiers_text = '', ''
+                old_annotation_text = ''
+                old_annotation_qualifiers = Qualifiers()
                 if LocalBranchShortName.of(pr.head) in self.__annotations:
-                    old_annotation_text = self.__annotations[LocalBranchShortName.of(pr.head)].text_without_qualifiers
-                    old_annotation_qualifiers_text = self.__annotations[LocalBranchShortName.of(pr.head)].qualifiers_text
+                    old_annotation = self.__annotations[LocalBranchShortName.of(pr.head)]
+                    old_annotation_text = old_annotation.text_without_qualifiers
+                    old_annotation_qualifiers = old_annotation.qualifiers
 
-                if pr.user != current_user and old_annotation_qualifiers_text == '':
+                if pr.user != current_user and old_annotation_qualifiers.is_default():
                     if verbose:
                         print(fmt(f'Annotating {bold(pr.head)} as `{anno} rebase=no push=no`'))
-                    self.__annotations[LocalBranchShortName.of(pr.head)] = Annotation(f'{anno} rebase=no push=no')
+                    self.__annotations[LocalBranchShortName.of(pr.head)] = Annotation(anno, Qualifiers(rebase=False, push=False))
                 elif old_annotation_text != anno:
                     if verbose:
                         print(fmt(f'Annotating {bold(pr.head)} as `{anno}`'))
-                    self.__annotations[LocalBranchShortName.of(pr.head)] = Annotation(f'{anno} {old_annotation_qualifiers_text}') \
-                        if old_annotation_text is not None else Annotation(anno)
+                    self.__annotations[LocalBranchShortName.of(pr.head)] = Annotation(anno, old_annotation_qualifiers)
             else:
                 debug(f'{pr} does NOT correspond to a managed branch')
         self.save_branch_layout_file()
@@ -2535,11 +2598,9 @@ class MacheteClient:
             print(f'Description of {pr.display_text()} has been updated')
 
         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(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))
+        anno = self.__annotations.get(head)
+        self.__annotations[head] = Annotation(self.__pull_request_annotation(
+            spec, pr, current_user), anno.qualifiers if anno else Qualifiers())
         self.save_branch_layout_file()
 
         if opt_update_related_descriptions:
@@ -2900,7 +2961,7 @@ class MacheteClient:
             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.__annotations[head] = Annotation(self.__pull_request_annotation(spec, pr, current_user), qualifiers=Qualifiers())
         self.save_branch_layout_file()
 
         if opt_update_related_descriptions:
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>
diff --git a/requirements/coverage.txt b/requirements/coverage.txt
index 9e8c60e..124d9e7 100644
--- a/requirements/coverage.txt
+++ b/requirements/coverage.txt
@@ -1,7 +1,7 @@
 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' and python_version < '3.9'
-coverage==7.6.8; python_version >= '3.9'
+coverage==7.6.10; 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'
diff --git a/requirements/mypy.txt b/requirements/mypy.txt
index 2c98de8..132b08f 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.13.0; python_version >= '3.8'
+mypy==1.14.1; python_version >= '3.8'
diff --git a/requirements/py-docs.txt b/requirements/py-docs.txt
index e5cdb9f..36ff64f 100644
--- a/requirements/py-docs.txt
+++ b/requirements/py-docs.txt
@@ -1,3 +1,3 @@
 docutils==0.21.2
-pygments==2.18.0
+pygments==2.19.1
 beautifulsoup4==4.12.3
diff --git a/requirements/pypi-publish.txt b/requirements/pypi-publish.txt
index 29dbc1d..596f1d7 100644
--- a/requirements/pypi-publish.txt
+++ b/requirements/pypi-publish.txt
@@ -1,4 +1,4 @@
 build==1.2.2.post1
-setuptools==75.6.0
+setuptools==75.8.0
 twine==6.0.1
 wheel==0.45.1
diff --git a/requirements/sphinx-docs.txt b/requirements/sphinx-docs.txt
index 6a7b8c5..dc480ff 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.16.0
+pydata-sphinx-theme==0.16.1
 
 sphinx==8.1.3
 sphinx-book-theme==1.1.3
diff --git a/requirements/typos.txt b/requirements/typos.txt
index 3b9621e..6e4c845 100644
--- a/requirements/typos.txt
+++ b/requirements/typos.txt
@@ -1,1 +1,1 @@
-typos==1.28.1
+typos==1.29.4
diff --git a/requirements/vulture-check.txt b/requirements/vulture-check.txt
index e38c42e..127cc32 100644
--- a/requirements/vulture-check.txt
+++ b/requirements/vulture-check.txt
@@ -1,1 +1,1 @@
-vulture==2.13
+vulture==2.14


Test patch:
diff --git a/ci/deb-ppa-test-install/Dockerfile b/ci/deb-ppa-test-install/Dockerfile
deleted file mode 100644
index bf3022c..0000000
--- a/ci/deb-ppa-test-install/Dockerfile
+++ /dev/null
@@ -1,15 +0,0 @@
-ARG ubuntu_version
-FROM ubuntu:${ubuntu_version}
-SHELL ["/bin/bash", "-e", "-o", "pipefail", "-u", "-x", "-c"]
-
-ARG launchpad_username
-# See https://github.com/phusion/baseimage-docker/issues/58 for why DEBIAN_FRONTEND=noninteractive is needed (20.04-only issue)
-RUN \
-    apt-get update; \
-    DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y git gpg-agent software-properties-common; \
-    add-apt-repository ppa:$launchpad_username/git-machete; \
-    apt-get autoremove --purge -y gpg-agent software-properties-common
-
-COPY entrypoint.sh /
-RUN chmod +x /entrypoint.sh
-CMD ["/entrypoint.sh"]
diff --git a/ci/deb-ppa-test-install/README.md b/ci/deb-ppa-test-install/README.md
deleted file mode 100644
index abafdb3..0000000
--- a/ci/deb-ppa-test-install/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-## Test the availability of the uploaded package
-
-```shell script
-cd ci/deb-ppa-test-install/
-export LAUNCHPAD_USERNAME=pawel-lipski  # or virtuslab
-export UBUNTU_VERSION=18.04  # or 20.04 or 22.04
-docker-compose build deb-ppa-test-install
-docker-compose run deb-ppa-test-install
-```
diff --git a/ci/deb-ppa-test-install/build-context/entrypoint.sh b/ci/deb-ppa-test-install/build-context/entrypoint.sh
deleted file mode 100644
index 2db3ea6..0000000
--- a/ci/deb-ppa-test-install/build-context/entrypoint.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env bash
-
-set -e -o pipefail -u -x
-
-apt-get update
-apt-get install --no-install-recommends -y python3-git-machete
-git machete --version
-apt-get autoremove --purge -y python3-git-machete
-! command -v git-machete
diff --git a/ci/deb-ppa-test-install/docker-compose.yml b/ci/deb-ppa-test-install/docker-compose.yml
deleted file mode 100644
index 9b84888..0000000
--- a/ci/deb-ppa-test-install/docker-compose.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-services:
-  deb-ppa-test-install:
-    image: gitmachete/ci-deb-ppa-install:${UBUNTU_VERSION}
-    platform: linux/amd64
-    build:
-      context: build-context
-      dockerfile: ../Dockerfile # relative to build-context
-      args:
-        launchpad_username: ${LAUNCHPAD_USERNAME}
-        ubuntu_version: ${UBUNTU_VERSION}
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_client.py b/tests/test_client.py
index 5dc8754..123cb61 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -62,50 +62,50 @@ class TestClient(BaseTest):
         annotations: Dict[LocalBranchShortName, Annotation] = machete_client.annotations
 
         feature_2_branch = LocalBranchShortName.of('feature2')
-        assert annotations[feature_2_branch].text == 'annotation'
+        assert annotations[feature_2_branch].unformatted_full_text == 'annotation'
         assert annotations[feature_2_branch].qualifiers.rebase is True
         assert annotations[feature_2_branch].qualifiers.push is True
         assert annotations[feature_2_branch].text_without_qualifiers == 'annotation'
-        assert annotations[feature_2_branch].qualifiers_text == ''
+        assert str(annotations[feature_2_branch].qualifiers) == ''
 
         feature_3_branch = LocalBranchShortName.of('feature3')
-        assert annotations[feature_3_branch].text == 'annotation rebase=no push=no'
+        assert annotations[feature_3_branch].unformatted_full_text == 'annotation rebase=no push=no'
         assert annotations[feature_3_branch].qualifiers.rebase is False
         assert annotations[feature_3_branch].qualifiers.push is False
         assert annotations[feature_3_branch].text_without_qualifiers == 'annotation'
-        assert annotations[feature_3_branch].qualifiers_text == 'rebase=no push=no'
+        assert str(annotations[feature_3_branch].qualifiers) == 'rebase=no push=no'
 
         feature_4_branch = LocalBranchShortName.of('feature4')
-        assert annotations[feature_4_branch].text == 'rebase=no push=no annotation'
+        assert annotations[feature_4_branch].unformatted_full_text == 'annotation rebase=no push=no'
         assert annotations[feature_4_branch].qualifiers.rebase is False
         assert annotations[feature_4_branch].qualifiers.push is False
         assert annotations[feature_4_branch].text_without_qualifiers == 'annotation'
-        assert annotations[feature_4_branch].qualifiers_text == 'rebase=no push=no'
+        assert str(annotations[feature_4_branch].qualifiers) == 'rebase=no push=no'
 
         feature_5_branch = LocalBranchShortName.of('feature5')
-        assert annotations[feature_5_branch].text == 'annotation1 rebase=no annotation2 push=no annotation3'
+        assert annotations[feature_5_branch].unformatted_full_text == 'annotation1 annotation2 annotation3 rebase=no push=no'
         assert annotations[feature_5_branch].qualifiers.rebase is False
         assert annotations[feature_5_branch].qualifiers.push is False
         assert annotations[feature_5_branch].text_without_qualifiers == 'annotation1 annotation2 annotation3'
-        assert annotations[feature_5_branch].qualifiers_text == 'rebase=no push=no'
+        assert str(annotations[feature_5_branch].qualifiers) == 'rebase=no push=no'
 
         feature_6_branch = LocalBranchShortName.of('feature6')
-        assert annotations[feature_6_branch].text == 'annotation1 rebase=nopush=no annotation2'
+        assert annotations[feature_6_branch].unformatted_full_text == 'annotation1 rebase=nopush=no annotation2'
         assert annotations[feature_6_branch].qualifiers.rebase is True
         assert annotations[feature_6_branch].qualifiers.push is True
         assert annotations[feature_6_branch].text_without_qualifiers == 'annotation1 rebase=nopush=no annotation2'
-        assert annotations[feature_6_branch].qualifiers_text == ''
+        assert str(annotations[feature_6_branch].qualifiers) == ''
 
         feature_7_branch = LocalBranchShortName.of('feature7')
-        assert annotations[feature_7_branch].text == 'annotation1rebase=no push=noannotation2'
+        assert annotations[feature_7_branch].unformatted_full_text == 'annotation1rebase=no push=noannotation2'
         assert annotations[feature_7_branch].qualifiers.rebase is True
         assert annotations[feature_7_branch].qualifiers.push is True
         assert annotations[feature_7_branch].text_without_qualifiers == 'annotation1rebase=no push=noannotation2'
-        assert annotations[feature_7_branch].qualifiers_text == ''
+        assert str(annotations[feature_7_branch].qualifiers) == ''
 
         feature_8_branch = LocalBranchShortName.of('feature8')
-        assert annotations[feature_8_branch].text == 'annotation rebase=no push=no rebase=no push=no'
+        assert annotations[feature_8_branch].unformatted_full_text == 'annotation rebase=no push=no'
         assert annotations[feature_8_branch].qualifiers.rebase is False
         assert annotations[feature_8_branch].qualifiers.push is False
         assert annotations[feature_8_branch].text_without_qualifiers == 'annotation'
-        assert annotations[feature_8_branch].qualifiers_text == 'rebase=no push=no'
+        assert str(annotations[feature_8_branch].qualifiers) == 'rebase=no push=no'
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")