Repository: VirtusLab/git-machete
Pull request #1335 (2024-09-18 17:34:44)

Release v3.29.3


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 575ae80..7afa381 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -150,13 +150,13 @@ jobs:
           root: .
           paths: [ .coverage.* ]
 
-  "python 3_12 git 2_42_0":
+  "python 3_12 git 2_46_1":
     executor: ubuntu_executor
     steps:
       - checkout
       - run:
-          name: Run tests against Python 3.12 and git 2.42.0
-          command: PYTHON_VERSION=3.12 GIT_VERSION=2.42.0  bash ci/tox/ci-run.sh
+          name: Run tests against Python 3.12 and git 2.46.1
+          command: PYTHON_VERSION=3.12 GIT_VERSION=2.46.1  bash ci/tox/ci-run.sh
       - store_test_results:
           path: test-results/
       - persist_to_workspace:
@@ -193,16 +193,21 @@ jobs:
       - codecov/upload:
           file: coverage.xml
 
-  "dry run deploy":
+  "dry run releases":
     executor: ubuntu_executor
     steps:
       - checkout
 
       - run:
-          name: '[Dry run] Deploy to PyPI'
+          name: '[Dry run] Publish to PyPI'
           # Side effects: does an actual release, but to the test PyPI
           command: bash ci/pypi/ci-deploy.sh --dry-run
 
+      - run:
+          name: '[Dry run] Build Arch Linux package'
+          # 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
@@ -218,18 +223,12 @@ jobs:
           name: '[Dry run] Build snap'
           command: bash ci/snap/ci-deploy.sh --dry-run
 
-      - run:
-          name: '[Dry run] Build rpm package'
-          # Side effects: potentially pushes a regenerated Docker image
-          # Other than that, it just builds the package without publishing anything; there's no special dry-run mode
-          command: bash ci/rpm/ci-run.sh
-
-  "deploy to pypi":
+  "publish to pypi":
     executor: ubuntu_executor
     steps:
       - checkout
       - run:
-          name: 'Deploy to PyPI'
+          name: 'Publish to PyPI'
           command: bash ci/pypi/ci-deploy.sh
 
   "build and upload deb package to ppa":
@@ -248,18 +247,12 @@ jobs:
           name: 'Publish snap to the stable channel'
           command: bash ci/snap/ci-deploy.sh
 
-  "build rpm package and create github release":
+  "create github release":
     executor: ubuntu_executor
     steps:
       - checkout
-      - run:
-          name: 'Build rpm package'
-          command: bash ci/rpm/ci-run.sh
-      # Let's do this at the very end to avoid opening a GH release (and notifying the watchers)
-      # in case anything goes wrong in the previous steps.
       - run:
           name: 'Create GitHub release'
-          # Prerequisites: RPM package must be in dist/
           # language=sh
           command: |
             sudo apt-get update
@@ -277,7 +270,7 @@ test_jobs: &test_jobs
   - python 3_9 git 2_30_1
   - python 3_10 git 2_33_1
   - python 3_11 git 2_38_1
-  - python 3_12 git 2_42_0
+  - python 3_12 git 2_46_1
   - windows tests
 
 only_master: &only_master
@@ -296,44 +289,42 @@ workflows:
       - python 3_9 git 2_30_1
       - python 3_10 git 2_33_1
       - python 3_11 git 2_38_1
-      - python 3_12 git 2_42_0
+      - python 3_12 git 2_46_1
       - windows tests
       - coverage upload:
           requires:
             *test_jobs
-      - dry run deploy:
+      - dry run releases:
           filters:
             branches:
               only:
+                - "/.*(ci|deploy|dry-run|publish|release).*/"
                 - "/dependabot.pip.*/"
                 - "develop"
-                - "/.*dry-run.*/"
                 - "/hotfix.*/"
                 - "master"
           requires:
             *test_jobs
 
-      # Let's keep each deployment in a separate job (rather than in separate step of a single job),
-      # so that CircleCI allows for re-running just the failed deployments in case of a transient failure.
-      - deploy to pypi:
+      # Let's keep each release in a separate job (rather than in separate step of a single job),
+      # so that CircleCI allows for re-running just the failed releases in case of a transient failure.
+      - publish to pypi:
           <<: *only_master
           requires:
-            - dry run deploy
+            - dry run releases
       - build and upload deb package to ppa:
           <<: *only_master
           requires:
-            - dry run deploy
+            - dry run releases
       - publish snap to the stable channel:
           <<: *only_master
           requires:
-            - dry run deploy
-      # Let's do this at the very end to avoid opening a GH release (and notifying the watchers)
-      # in case anything goes wrong in the previous steps.
-      - build rpm package and create github release:
+            - dry run releases
+      # Let's perform a GitHub release even if some of the preceding release steps failed,
+      # to keep GitHub release history consistent
+      - create github release:
           <<: *only_master
           requires:
-            - deploy to pypi
-            - build and upload deb package to ppa
-            - publish snap to the stable channel
+            - dry run releases
 
       # Note: there are also releases happening outside of our CI, see PACKAGES.md
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 08a6623..3f0d058 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,7 +4,9 @@
 version: 2
 updates:
   - package-ecosystem: "pip"
-    directory: "/requirements/"
+    directories:
+      - /requirements
+      - /ci/tox/build-context
     schedule:
       interval: weekly
     reviewers:
@@ -13,4 +15,5 @@ updates:
     groups:
       deps:
         patterns:
+          # To lump all updated dependencies together into a single PR (instead of PR-per-dependency)
           - "*"
diff --git a/PACKAGES.md b/PACKAGES.md
index 3f56259..5f55fe5 100644
--- a/PACKAGES.md
+++ b/PACKAGES.md
@@ -7,10 +7,10 @@ the **edge-most** channel that corresponds to the **released** versions of git-m
 | 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                            | ![AUR last modified](https://img.shields.io/aur/last-modified/git-machete.svg)                                                                                                                                                                                                                                        |
+| 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 (rpm)                               | ![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]                                                                                                                                                                                                           |
+| 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                            |                                                                                                                                                                                                                                                                                                                       |
@@ -26,7 +26,7 @@ the **edge-most** channel that corresponds to the **released** versions of git-m
 
 [^1]: If a release introduces significant changes/critical bugfixes, we should [flag the package as out of date](https://aur.archlinux.org/pkgbase/git-machete/flag).
 
-[^2]: GitHub downloads most likely correspond to the RPM packages attached to the releases.
+[^2]: GitHub downloads most likely correspond to the RPM packages attached to the releases up to v3.29.2.
 
 [^3]: Homebrew updates used to be done by our CI until git-machete has been added to [`.github/autobump.txt` in `Homebrew/homebrew-core`](https://github.com/Homebrew/homebrew-core/blob/master/.github/autobump.txt)
       on [PR #171225 to Homebrew/homebrew-core](https://github.com/Homebrew/homebrew-core/pull/171225/files#diff-895127b46978b016c724afe1fe53341d3d25253eead3b82d796b9bd72aa0c9a9R840).
diff --git a/README.md b/README.md
index 8f09468..e8ccb77 100644
--- a/README.md
+++ b/README.md
@@ -105,11 +105,6 @@ sudo apt-get update
 sudo apt-get install -y python3-git-machete
 ```
 
-### Using rpm (Fedora/RHEL/CentOS/openSUSE...)
-
-Download the rpm package from the [latest release](https://github.com/VirtusLab/git-machete/releases/latest)
-and install either by opening it in your desktop environment or with `rpm -i git-machete-*.noarch.rpm`.
-
 ### 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 f2be65f..142390c 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,11 @@
 # Release notes
 
+## New in git-machete 3.29.3
+
+- changed: no longer publish RPM files with GitHub releases
+- fixed: work around the parsing bug in `git patch-id` v2.46.1 (reported by @ilai-deutel)
+- fixed: `git machete git{hub,lab} create-{pr,mr}` takes into account `<!-- {start,end} git-machete generated -->` in PR/MR template (suggested by @frank-west-iii)
+
 ## New in git-machete 3.29.2
 
 - changed: no longer publish Docker images
diff --git a/blogs/docker-ci-tricks-1/blog.md b/blogs/docker-ci-tricks-1/blog.md
index 66a6e42..d811974 100644
--- a/blogs/docker-ci-tricks-1/blog.md
+++ b/blogs/docker-ci-tricks-1/blog.md
@@ -164,7 +164,6 @@ fi
 
 tox -e $TOX_ENV_LIST
 
-$PYTHON setup.py install --user
 git machete --version
 ```
 
diff --git a/ci/aur/Dockerfile b/ci/aur/Dockerfile
new file mode 100644
index 0000000..27ab1c7
--- /dev/null
+++ b/ci/aur/Dockerfile
@@ -0,0 +1,13 @@
+
+FROM archlinux:base-devel
+RUN pacman -Sy --noconfirm git
+
+RUN useradd -m builder && \
+    echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder && \
+    chmod 0440 /etc/sudoers.d/builder
+USER builder
+WORKDIR /home/builder
+
+COPY --chown=builder entrypoint.sh .
+RUN chmod +x entrypoint.sh
+CMD /home/builder/entrypoint.sh
diff --git a/ci/aur/build-context/entrypoint.sh b/ci/aur/build-context/entrypoint.sh
new file mode 100644
index 0000000..19d9f61
--- /dev/null
+++ b/ci/aur/build-context/entrypoint.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+git clone https://aur.archlinux.org/git-machete.git
+cd git-machete/
+
+sed -i "s/.*pytest  *-m.*/  pytest --numprocesses=auto -m 'not completion_e2e' -vv/" PKGBUILD
+sed -i "s/checkdepends=.*/checkdepends=('python-pytest' 'python-pytest-mock' 'python-pytest-xdist')/" PKGBUILD
+sed -i "s/pkgver=.*/pkgver=$GIT_REVISION/" PKGBUILD
+
+url=https://github.com/VirtusLab/git-machete/archive/$GIT_REVISION.tar.gz
+sed -i "s!::[^\"]*!::$url!" PKGBUILD
+hash=$(curl -L -s $url | sha256sum | head -c 64)
+sed -i "s/sha256sums=.*/sha256sums=('$hash')/" PKGBUILD
+cat PKGBUILD
+
+makepkg --syncdeps --log --install --clean --noconfirm
diff --git a/ci/aur/ci-run.sh b/ci/aur/ci-run.sh
new file mode 100644
index 0000000..9a5e411
--- /dev/null
+++ b/ci/aur/ci-run.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u -x
+
+source "$(git rev-parse --show-toplevel)"/ci/ci-run-commons.sh
+
+git_revision=$(git rev-parse HEAD)
+docker_compose_pull_or_build_and_push aur
+docker-compose --ansi=never run -e GIT_REVISION="$git_revision" aur
diff --git a/ci/aur/docker-compose.yml b/ci/aur/docker-compose.yml
new file mode 100644
index 0000000..574c75d
--- /dev/null
+++ b/ci/aur/docker-compose.yml
@@ -0,0 +1,10 @@
+services:
+  aur:
+    image: gitmachete/ci-aur:${DIRECTORY_HASH:-unspecified}
+    platform: linux/amd64
+    build:
+      context: build-context
+      dockerfile: ../Dockerfile # relative to build-context
+    tty: true
+    environment:
+      - GIT_REVISION
diff --git a/ci/aur/local-run.sh b/ci/aur/local-run.sh
new file mode 100755
index 0000000..a59dc37
--- /dev/null
+++ b/ci/aur/local-run.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u -x
+
+source "$(git rev-parse --show-toplevel)"/ci/local-run-commons.sh aur
+
+export_directory_hash aur
+cd "$(git rev-parse --show-toplevel)"/ci/aur/
+
+git_revision=$(git rev-parse "@{upstream}")
+docker-compose --progress=plain build aur
+docker-compose run -e GIT_REVISION="$git_revision" aur
diff --git a/ci/ci-run-commons.sh b/ci/ci-run-commons.sh
index 2bd4bb4..8e12cb1 100644
--- a/ci/ci-run-commons.sh
+++ b/ci/ci-run-commons.sh
@@ -4,7 +4,7 @@ set -e -o pipefail -u
 
 function retry() {
   attempts=$1
-  interval=10
+  interval=5
   for i in $(seq 1 $attempts); do
     if "${@:2}"; then break; fi
 
diff --git a/ci/deb-ppa-upload/docker-compose.yml b/ci/deb-ppa-upload/docker-compose.yml
index 1bfbc4d..05a3f47 100644
--- a/ci/deb-ppa-upload/docker-compose.yml
+++ b/ci/deb-ppa-upload/docker-compose.yml
@@ -1,7 +1,7 @@
-version: '3'
 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
diff --git a/ci/github-releases/ci-deploy.sh b/ci/github-releases/ci-deploy.sh
index df64737..3c65322 100644
--- a/ci/github-releases/ci-deploy.sh
+++ b/ci/github-releases/ci-deploy.sh
@@ -7,4 +7,3 @@ tag=v$(cut -d\' -f2 git_machete/__init__.py)
 # (since apparently all non-draft releases on GitHub must have a corresponding git tag).
 hub release create "$tag" \
   --message="$tag"$'\n\n'"$(sed '5,/^$/!d; /^$/d' RELEASE_NOTES.md)" \
-  --attach="$(echo dist/git-machete-*.noarch.rpm)"
diff --git a/ci/nixpkgs-build/docker-compose.yml b/ci/nixpkgs-build/docker-compose.yml
index f553c58..22ed006 100644
--- a/ci/nixpkgs-build/docker-compose.yml
+++ b/ci/nixpkgs-build/docker-compose.yml
@@ -1,7 +1,7 @@
-version: '3'
 services:
   nixpkgs-build:
     image: gitmachete/ci-nixpkgs-build:${DIRECTORY_HASH:-unspecified}
+    platform: linux/amd64
     build:
       context: build-context
       dockerfile: ../Dockerfile # relative to build-context
diff --git a/ci/pypi/ci-deploy.sh b/ci/pypi/ci-deploy.sh
index 86a05a0..3a8b5cc 100644
--- a/ci/pypi/ci-deploy.sh
+++ b/ci/pypi/ci-deploy.sh
@@ -5,6 +5,18 @@ set -e -o pipefail -u
 if [[ ${1-} == "--dry-run" || ${CIRCLE_BRANCH-} != "master" ]]; then
   repository=testpypi
   token=$TEST_PYPI_TOKEN
+  set -x
+  current_version=$(cut -d\' -f2 git_machete/__init__.py)
+  latest_pypi_version=$(curl -s "https://test.pypi.org/pypi/git-machete/json" | jq -r '.releases | keys | .[]' | sort -V | tail -1)
+  if [[ ${latest_pypi_version} == "$current_version".post* ]]; then
+    ordinal=${latest_pypi_version#*.post}
+    ordinal=$((ordinal+1))
+  else
+    ordinal=1
+  fi
+  sed -i "s/'$/-$ordinal'/" git_machete/__init__.py
+  cat git_machete/__init__.py
+  set +x
 else
   repository=pypi
   token=$PYPI_TOKEN
@@ -17,7 +29,6 @@ password = $token
 EOF
 
 set -x
-pip3 install -r requirements/pypi-deploy.txt
-python3 setup.py sdist
-python3 setup.py bdist_wheel
-python3 -m twine upload --repository $repository --skip-existing dist/*
+pip3 install -r requirements/pypi-publish.txt
+python3 -m build
+python3 -m twine upload --repository $repository --skip-existing --verbose dist/*
diff --git a/ci/rpm/Dockerfile b/ci/rpm/Dockerfile
deleted file mode 100644
index 7499302..0000000
--- a/ci/rpm/Dockerfile
+++ /dev/null
@@ -1,11 +0,0 @@
-FROM fedora:35
-SHELL ["/bin/bash", "-e", "-o", "pipefail", "-u", "-x", "-c"]
-
-RUN dnf update -y
-RUN dnf install -y git python3-pip rpm-build
-
-COPY entrypoint.sh run.sh /
-RUN chmod +x /entrypoint.sh /run.sh
-ENTRYPOINT ["/entrypoint.sh"]
-CMD ["/run.sh"]
-WORKDIR /git-machete
diff --git a/ci/rpm/README.md b/ci/rpm/README.md
deleted file mode 100644
index cab97cd..0000000
--- a/ci/rpm/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-## Building the RPM locally
-
-```shell script
-cd ci/rpm/
-./local-run.sh
-```
-
-The artifacts will be located in dist/ folder in top-level project directory.
diff --git a/ci/rpm/build-context/entrypoint.sh b/ci/rpm/build-context/entrypoint.sh
deleted file mode 100644
index 1f15099..0000000
--- a/ci/rpm/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/rpm/build-context/run.sh b/ci/rpm/build-context/run.sh
deleted file mode 100644
index da0fa68..0000000
--- a/ci/rpm/build-context/run.sh
+++ /dev/null
@@ -1,16 +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
-
-python3 setup.py bdist_rpm
-rpm -i dist/git-machete-*.rpm
-git machete version
-if ! git machete completion bash | grep '#!.*bash'; then
-  echo "shell completion is not available in runtime, aborting"
-  exit 1
-fi
diff --git a/ci/rpm/ci-run.sh b/ci/rpm/ci-run.sh
deleted file mode 100644
index b346608..0000000
--- a/ci/rpm/ci-run.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-
-set -e -o pipefail -u -x
-
-source "$(git rev-parse --show-toplevel)"/ci/ci-run-commons.sh
-
-docker_compose_pull_or_build_and_push rpm
-docker-compose --ansi=never run rpm
diff --git a/ci/rpm/docker-compose.yml b/ci/rpm/docker-compose.yml
deleted file mode 100644
index 363bfc9..0000000
--- a/ci/rpm/docker-compose.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-version: '3'
-services:
-  rpm:
-    image: gitmachete/ci-rpm:${DIRECTORY_HASH:-unspecified}
-    build:
-      context: build-context
-      dockerfile: ../Dockerfile # relative to build-context
-    volumes:
-      # Host path is relative to current directory, not build-context
-      - ./../..:/git-machete
diff --git a/ci/rpm/local-run.sh b/ci/rpm/local-run.sh
deleted file mode 100755
index 37c0ffb..0000000
--- a/ci/rpm/local-run.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-
-set -e -o pipefail -u
-
-source "$(git rev-parse --show-toplevel)"/ci/local-run-commons.sh rpm
-
-export_directory_hash rpm
-cd "$(git rev-parse --show-toplevel)"/ci/rpm/
-
-set -x
-docker-compose --progress=plain build rpm
-docker-compose run -e GID="$(id -g)" -e UID="$(id -u)" rpm
diff --git a/ci/tox/Dockerfile b/ci/tox/Dockerfile
index 567701b..830ca26 100644
--- a/ci/tox/Dockerfile
+++ b/ci/tox/Dockerfile
@@ -27,8 +27,9 @@ RUN \
 ARG python_version
 ENV PYTHON_VERSION=${python_version}
 ENV PYTHON=python${python_version}
-RUN apk add --no-cache gcc musl-dev  # packages required to install mypy
-RUN $PYTHON -m pip install tox setuptools
+RUN apk add --no-cache gcc musl-dev  # packages required to install mypy (done via tox in running container, not here)
+COPY requirements.txt /
+RUN $PYTHON -m pip install -r /requirements.txt
 
 RUN apk add --no-cache bash sudo  # packages required to run the entrypoint script
 ENV PATH=$PATH:/root/.local/bin/
diff --git a/ci/tox/build-context/requirements.txt b/ci/tox/build-context/requirements.txt
new file mode 100644
index 0000000..1e06592
--- /dev/null
+++ b/ci/tox/build-context/requirements.txt
@@ -0,0 +1,11 @@
+build==0.9.0; python_version <= '3.6'
+build==1.1.1; python_version >= '3.7' and python_version < '3.8'
+build==1.2.2; 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==74.1.2; python_version >= '3.8'
+
+tox==3.28.0; python_version <= '3.6'
+tox==4.8.0; python_version >= '3.7' and python_version < '3.8'
+tox==4.18.1; python_version >= '3.8'
diff --git a/ci/tox/build-context/run.sh b/ci/tox/build-context/run.sh
index 94ce717..bb12cc7 100644
--- a/ci/tox/build-context/run.sh
+++ b/ci/tox/build-context/run.sh
@@ -13,7 +13,7 @@ set -x
 
 tox -e mypy,coverage
 
-$PYTHON setup.py sdist bdist_wheel
+$PYTHON -m build
 
 ls -l dist/
 # Name of the generated tar.gz changed in a certain setuptools/Python version from git-machete-... to git_machete-...,
@@ -22,8 +22,6 @@ tar tvf dist/git*machete-*.tar.gz | grep docs/man/git-machete.1
 unzip -v dist/git_machete-*.whl   | grep docs/man/git-machete.1
 
 $PYTHON -m venv venv/sdist/
-$PYTHON -m venv venv/bdist_wheel/
-
 . venv/sdist/bin/activate
 pip install dist/git*machete-*.tar.gz
 git machete version
@@ -32,6 +30,7 @@ if ! git machete completion fish | grep 'complete -c git-machete'; then
   exit 1
 fi
 
+$PYTHON -m venv venv/bdist_wheel/
 . venv/bdist_wheel/bin/activate
 pip install dist/git_machete-*.whl
 git machete version
diff --git a/ci/tox/docker-compose.yml b/ci/tox/docker-compose.yml
index e99568a..8a4cebf 100644
--- a/ci/tox/docker-compose.yml
+++ b/ci/tox/docker-compose.yml
@@ -1,7 +1,7 @@
-version: '3'
 services:
   tox:
     image: gitmachete/ci-tox:git${GIT_VERSION}-python${PYTHON_VERSION}-${DIRECTORY_HASH:-unspecified}
+    platform: linux/amd64
     build:
       context: build-context
       dockerfile: ../Dockerfile # relative to build-context
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 9ee5a2c..eff9283 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" "Sep 04, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Sep 18, 2024" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.29.2
+git-machete \- git-machete 3.29.3
 .sp
 git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
 .sp
@@ -363,7 +363,7 @@ from the message body of the first unique commit of the branch, even if \fB\&.gi
 .B \fBmachete.github.prDescriptionIntroStyle\fP:
 .INDENT 7.0
 .TP
-.B Select the style of the intro prepended to PR description:
+.B Select the style of the generated section (\(dqintro\(dq) added to the PR description:
 .INDENT 7.0
 .IP \(bu 2
 \fBfull\fP    \-\-\- include both a chain of upstream PRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream PRs
@@ -409,7 +409,7 @@ from the message body of the first unique commit of the branch, even if \fB\&.gi
 .B \fBmachete.gitlab.mrDescriptionIntroStyle\fP:
 .INDENT 7.0
 .TP
-.B Select the style of the intro prepended to MR description:
+.B Select the style of the generated section (\(dqintro\(dq) added to the MR description:
 .INDENT 7.0
 .IP \(bu 2
 \fBfull\fP    \-\-\- include both a chain of upstream MRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream MRs
@@ -999,11 +999,13 @@ If \fB\&.git/info/milestone\fP file is present, its contents (a single number \-
 If \fB\&.git/info/reviewers\fP file is present, its contents (one GitHub login per line) are used to set reviewers.
 .sp
 The subject of the first unique commit of the branch is used as PR title.
-If \fB\&.git/info/description\fP or \fB\&.github/pull_request_template.md\fP file is present, its contents are used as PR description.
+If \fB\&.git/info/description\fP or \fB\&.github/pull_request_template.md\fP template is present, its contents are used as PR description.
 Otherwise (or if \fBmachete.github.forceDescriptionFromCommitMessage\fP is set), PR description is taken from message body of the first unique commit of the branch.
 .sp
-If the newly\-created PR is stacked atop another PR, the actual PR description posted to GitHub will be prepended with a section
-listing the entire related chain of PRs.
+If the newly\-created PR is stacked atop another PR, the actual PR description posted to GitHub will include a generated section (\(dqintro\(dq)
+listing the entire related chain of PRs. This section will be delimited with \fB<!\-\- start git\-machete generated \-\->\fP
+and \fB<!\-\- end git\-machete generated \-\->\fP comments in Markdown. If a PR template file exists and contains these comments already,
+the generated section will be placed between them; otherwise, it will be placed at the beginning.
 .sp
 \fBOptions:\fP
 .INDENT 7.0
@@ -1037,8 +1039,8 @@ Draft PRs don\(aqt get code owners automatically added as reviewers.
 .B \fBretarget\-pr [\-b|\-\-branch=<branch>] [\-\-ignore\-if\-missing]\fP:
 Sets the base of the current (or specified) branch\(aqs PR to upstream (parent) branch, as seen by git machete (see \fBgit machete show up\fP).
 .sp
-If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended
-with an intro section listing the entire related chain of PRs.
+If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will include
+a generated section (\(dqintro\(dq) listing the entire related chain of PRs.
 .sp
 This intro will be updated or removed accordingly with the subsequent runs of \fBretarget\-pr\fP, even if the base branch is already up to date.
 .sp
@@ -1107,7 +1109,7 @@ from the message body of the first unique commit of the branch, even if \fB\&.gi
 .B \fBmachete.github.prDescriptionIntroStyle\fP (\fBcreate\-pr\fP, \fBrestack\-pr\fP and \fBretarget\-pr\fP):
 .INDENT 7.0
 .TP
-.B Select the style of the intro prepended to PR description:
+.B Select the style of the generated section (\(dqintro\(dq) added to the PR description:
 .INDENT 7.0
 .IP \(bu 2
 \fBfull\fP    \-\-\- include both a chain of upstream PRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream PRs
@@ -1237,11 +1239,13 @@ Note that you need to use a global (not per\-project) milestone id. Look for som
 If \fB\&.git/info/reviewers\fP file is present, its contents (one GitLab login per line) are used to set reviewers.
 .sp
 The subject of the first unique commit of the branch is used as MR title.
-If \fB\&.git/info/description\fP or \fB\&.gitlab/merge_request_templates/Default.md\fP file is present, its contents are used as MR description.
+If \fB\&.git/info/description\fP or \fB\&.gitlab/merge_request_templates/Default.md\fP template is present, its contents are used as MR description.
 Otherwise (or if \fBmachete.gitlab.forceDescriptionFromCommitMessage\fP is set), MR description is taken from message body of the first unique commit of the branch.
 .sp
-If the newly\-created MR is stacked atop another MR, the actual MR description posted to GitLab will be prepended with a section
-listing the entire related chain of MRs.
+If the newly\-created MR is stacked atop another MR, the actual MR description posted to GitLab will include a generated section (\(dqintro\(dq)
+listing the entire related chain of MRs. This section will be delimited with \fB<!\-\- start git\-machete generated \-\->\fP
+and \fB<!\-\- end git\-machete generated \-\->\fP comments in Markdown. If an MR template file exists and contains these comments already,
+the generated section will be placed between them; otherwise, it will be placed at the beginning.
 .sp
 \fBOptions:\fP
 .INDENT 7.0
@@ -1275,8 +1279,8 @@ Draft MRs don\(aqt get code owners automatically added as reviewers.
 .B \fBretarget\-mr [\-b|\-\-branch=<branch>] [\-\-ignore\-if\-missing]\fP:
 Sets the target of the current (or specified) branch\(aqs MR to upstream (parent) branch, as seen by git machete (see \fBgit machete show up\fP).
 .sp
-If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended
-with an intro section listing the entire related chain of MRs.
+If after changing the base the MR ends up stacked atop another MR, the MR description posted to GitLab will include
+a generated section (\(dqintro\(dq) listing the entire related chain of MRs.
 .sp
 This intro will be updated or removed accordingly with the subsequent runs of \fBretarget\-mr\fP, even if the target branch is already up to date.
 .sp
@@ -1331,7 +1335,7 @@ from the message body of the first unique commit of the branch, even if \fB\&.gi
 .B \fBmachete.gitlab.mrDescriptionIntroStyle\fP (\fBcreate\-mr\fP, \fBrestack\-mr\fP and \fBretarget\-mr\fP):
 .INDENT 7.0
 .TP
-.B Select the style of the intro prepended to MR description:
+.B Select the style of the generated section (\(dqintro\(dq) added to the MR description:
 .INDENT 7.0
 .IP \(bu 2
 \fBfull\fP    \-\-\- include both a chain of upstream MRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream MRs
diff --git a/docs/source/cli/github.rst b/docs/source/cli/github.rst
index 272f8c3..5e0667b 100644
--- a/docs/source/cli/github.rst
+++ b/docs/source/cli/github.rst
@@ -84,11 +84,13 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
     If ``.git/info/reviewers`` file is present, its contents (one GitHub login per line) are used to set reviewers.
 
     The subject of the first unique commit of the branch is used as PR title.
-    If ``.git/info/description`` or ``.github/pull_request_template.md`` file is present, its contents are used as PR description.
+    If ``.git/info/description`` or ``.github/pull_request_template.md`` template is present, its contents are used as PR description.
     Otherwise (or if ``machete.github.forceDescriptionFromCommitMessage`` is set), PR description is taken from message body of the first unique commit of the branch.
 
-    If the newly-created PR is stacked atop another PR, the actual PR description posted to GitHub will be prepended with a section
-    listing the entire related chain of PRs.
+    If the newly-created PR is stacked atop another PR, the actual PR description posted to GitHub will include a generated section ("intro")
+    listing the entire related chain of PRs. This section will be delimited with ``<!-- start git-machete generated -->``
+    and ``<!-- end git-machete generated -->`` comments in Markdown. If a PR template file exists and contains these comments already,
+    the generated section will be placed between them; otherwise, it will be placed at the beginning.
 
     **Options:**
 
@@ -112,8 +114,8 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
 ``retarget-pr [-b|--branch=<branch>] [--ignore-if-missing]``:
     Sets the base of the current (or specified) branch's PR to upstream (parent) branch, as seen by git machete (see ``git machete show up``).
 
-    If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended
-    with an intro section listing the entire related chain of PRs.
+    If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will include
+    a generated section ("intro") listing the entire related chain of PRs.
 
     This intro will be updated or removed accordingly with the subsequent runs of ``retarget-pr``, even if the base branch is already up to date.
 
diff --git a/docs/source/cli/gitlab.rst b/docs/source/cli/gitlab.rst
index 947444b..a5d8bf4 100644
--- a/docs/source/cli/gitlab.rst
+++ b/docs/source/cli/gitlab.rst
@@ -85,11 +85,13 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
     If ``.git/info/reviewers`` file is present, its contents (one GitLab login per line) are used to set reviewers.
 
     The subject of the first unique commit of the branch is used as MR title.
-    If ``.git/info/description`` or ``.gitlab/merge_request_templates/Default.md`` file is present, its contents are used as MR description.
+    If ``.git/info/description`` or ``.gitlab/merge_request_templates/Default.md`` template is present, its contents are used as MR description.
     Otherwise (or if ``machete.gitlab.forceDescriptionFromCommitMessage`` is set), MR description is taken from message body of the first unique commit of the branch.
 
-    If the newly-created MR is stacked atop another MR, the actual MR description posted to GitLab will be prepended with a section
-    listing the entire related chain of MRs.
+    If the newly-created MR is stacked atop another MR, the actual MR description posted to GitLab will include a generated section ("intro")
+    listing the entire related chain of MRs. This section will be delimited with ``<!-- start git-machete generated -->``
+    and ``<!-- end git-machete generated -->`` comments in Markdown. If an MR template file exists and contains these comments already,
+    the generated section will be placed between them; otherwise, it will be placed at the beginning.
 
     **Options:**
 
@@ -113,8 +115,8 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
 ``retarget-mr [-b|--branch=<branch>] [--ignore-if-missing]``:
     Sets the target of the current (or specified) branch's MR to upstream (parent) branch, as seen by git machete (see ``git machete show up``).
 
-    If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended
-    with an intro section listing the entire related chain of MRs.
+    If after changing the base the MR ends up stacked atop another MR, the MR description posted to GitLab will include
+    a generated section ("intro") listing the entire related chain of MRs.
 
     This intro will be updated or removed accordingly with the subsequent runs of ``retarget-mr``, even if the target branch is already up to date.
 
diff --git a/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst b/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst
index 2948906..ea149d9 100644
--- a/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst
+++ b/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst
@@ -1,4 +1,4 @@
-Select the style of the intro prepended to PR description:
+Select the style of the generated section ("intro") added to the PR description:
   * ``full``    --- include both a chain of upstream PRs (typically leading to ``main``, ``master``, ``develop`` etc.) and a tree of downstream PRs
   * ``up-only`` --- default, include only a chain of upstream PRs
   * ``none``    --- prepend no intro to the PR description at all
diff --git a/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst b/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst
index 8465072..d19a8e4 100644
--- a/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst
+++ b/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst
@@ -1,4 +1,4 @@
-Select the style of the intro prepended to MR description:
+Select the style of the generated section ("intro") added to the MR description:
   * ``full``    --- include both a chain of upstream MRs (typically leading to ``main``, ``master``, ``develop`` etc.) and a tree of downstream MRs
   * ``up-only`` --- default, include only a chain of upstream MRs
   * ``none``    --- prepend no intro to the MR description at all
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index bbb48b2..1685d61 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.29.2'
+__version__ = '3.29.3'
diff --git a/git_machete/client.py b/git_machete/client.py
index 35daee7..1beb5bd 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -496,7 +496,7 @@ class MacheteClient:
                 non_root_fixed_branches_by_last_checkout_timestamps[-c:]
             stale_non_root_fixed_branches = [LocalBranchShortName.of(branch) for (timestamp, branch) in stale]
             if stale:
-                threshold_date = datetime.datetime.utcfromtimestamp(fresh[0][0]).strftime("%Y-%m-%d")
+                threshold_date = datetime.datetime.fromtimestamp(fresh[0][0], tz=datetime.timezone.utc).strftime("%Y-%m-%d")
                 warn(
                     f"to keep the size of the discovered tree reasonable (ca. {c} branches), "
                     f"only branches checked out at or after ca. <b>{threshold_date}</b> are included.\n"
@@ -2465,11 +2465,12 @@ class MacheteClient:
             start_index = lines.index(self.START_GIT_MACHETE_GENERATED_COMMENT)
             end_index = lines.index(self.END_GIT_MACHETE_GENERATED_COMMENT)
             lines = lines[:start_index] + lines_to_prepend + lines[end_index + 1:]
+            lines = skip_leading_empty(lines)
         else:
             # For compatibility with pre-v3.23.0 format; only affects GitHub
             if lines and '# Based on PR #' in lines[0]:
-                lines = skip_leading_empty(lines[1:])
-            lines = lines_to_prepend + lines
+                lines = lines[1:]
+            lines = lines_to_prepend + ([''] if lines_to_prepend else []) + skip_leading_empty(lines)
         return '\n'.join(lines)
 
     def retarget_pr(self, spec: CodeHostingSpec, head: LocalBranchShortName, ignore_if_missing: bool) -> None:
@@ -2792,14 +2793,11 @@ class MacheteClient:
         if base_branch_found_on_remote or style == PRDescriptionIntroStyle.FULL:
             # As the description may include the reference to this PR itself (in case of a chain of >=2 PRs),
             # let's update the PR description after it's already created (so that we know the current PR's number).
-            text_to_prepend = self.__generate_pr_description_intro(code_hosting_client, pr, style)
-            if text_to_prepend:
-                if description:
-                    text_to_prepend += '\n'
-                description = text_to_prepend + description
+            new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, description)
+            if new_description.strip() != description.strip():
                 print(f'Updating description of {pr.display_text()} to include '
                       f'the chain of {spec.pr_short_name}s... ', end='', flush=True)
-                code_hosting_client.set_description_of_pull_request(pr.number, description)
+                code_hosting_client.set_description_of_pull_request(pr.number, new_description)
                 print(fmt(ok_str))
 
         milestone_path: str = self.__git.get_main_git_subpath('info', 'milestone')
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index 4a7e904..f7f0200 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -251,7 +251,7 @@ long_docs: Dict[str, str] = {
 
            `machete.github.prDescriptionIntroStyle`:
 
-                 Select the style of the intro prepended to PR description:
+                 Select the style of the generated section ("intro") added to the PR description:
 
                  * `full`    — include both a chain of upstream PRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream PRs
 
@@ -288,7 +288,7 @@ long_docs: Dict[str, str] = {
 
            `machete.gitlab.mrDescriptionIntroStyle`:
 
-                 Select the style of the intro prepended to MR description:
+                 Select the style of the generated section ("intro") added to the MR description:
 
                  * `full`    — include both a chain of upstream MRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream MRs
 
@@ -639,11 +639,13 @@ long_docs: Dict[str, str] = {
               If `.git/info/reviewers` file is present, its contents (one GitHub login per line) are used to set reviewers.
 
               The subject of the first unique commit of the branch is used as PR title.
-              If `.git/info/description` or `.github/pull_request_template.md` file is present, its contents are used as PR description.
+              If `.git/info/description` or `.github/pull_request_template.md` template is present, its contents are used as PR description.
               Otherwise (or if `machete.github.forceDescriptionFromCommitMessage` is set), PR description is taken from message body of the first unique commit of the branch.
 
-              If the newly-created PR is stacked atop another PR, the actual PR description posted to GitHub will be prepended with a section
-              listing the entire related chain of PRs.
+              If the newly-created PR is stacked atop another PR, the actual PR description posted to GitHub will include a generated section ("intro")
+              listing the entire related chain of PRs. This section will be delimited with `<!-- start git-machete generated -->`
+              and `<!-- end git-machete generated -->` comments in Markdown. If a PR template file exists and contains these comments already,
+              the generated section will be placed between them; otherwise, it will be placed at the beginning.
 
               <b>Options:</b>
 
@@ -676,8 +678,8 @@ long_docs: Dict[str, str] = {
  
               Sets the base of the current (or specified) branch's PR to upstream (parent) branch, as seen by git machete (see `git machete show up`).
 
-              If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended
-              with an intro section listing the entire related chain of PRs.
+              If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will include
+              a generated section ("intro") listing the entire related chain of PRs.
 
               This intro will be updated or removed accordingly with the subsequent runs of `retarget-pr`, even if the base branch is already up to date.
 
@@ -737,7 +739,7 @@ long_docs: Dict[str, str] = {
 
            `machete.github.prDescriptionIntroStyle` (`create-pr`, `restack-pr` and `retarget-pr`):
 
-                 Select the style of the intro prepended to PR description:
+                 Select the style of the generated section ("intro") added to the PR description:
 
                  * `full`    — include both a chain of upstream PRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream PRs
 
@@ -830,11 +832,13 @@ long_docs: Dict[str, str] = {
               If `.git/info/reviewers` file is present, its contents (one GitLab login per line) are used to set reviewers.
 
               The subject of the first unique commit of the branch is used as MR title.
-              If `.git/info/description` or `.gitlab/merge_request_templates/Default.md` file is present, its contents are used as MR description.
+              If `.git/info/description` or `.gitlab/merge_request_templates/Default.md` template is present, its contents are used as MR description.
               Otherwise (or if `machete.gitlab.forceDescriptionFromCommitMessage` is set), MR description is taken from message body of the first unique commit of the branch.
 
-              If the newly-created MR is stacked atop another MR, the actual MR description posted to GitLab will be prepended with a section
-              listing the entire related chain of MRs.
+              If the newly-created MR is stacked atop another MR, the actual MR description posted to GitLab will include a generated section ("intro")
+              listing the entire related chain of MRs. This section will be delimited with `<!-- start git-machete generated -->`
+              and `<!-- end git-machete generated -->` comments in Markdown. If an MR template file exists and contains these comments already,
+              the generated section will be placed between them; otherwise, it will be placed at the beginning.
 
               <b>Options:</b>
 
@@ -867,8 +871,8 @@ long_docs: Dict[str, str] = {
  
               Sets the target of the current (or specified) branch's MR to upstream (parent) branch, as seen by git machete (see `git machete show up`).
 
-              If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended
-              with an intro section listing the entire related chain of MRs.
+              If after changing the base the MR ends up stacked atop another MR, the MR description posted to GitLab will include
+              a generated section ("intro") listing the entire related chain of MRs.
 
               This intro will be updated or removed accordingly with the subsequent runs of `retarget-mr`, even if the target branch is already up to date.
 
@@ -915,7 +919,7 @@ long_docs: Dict[str, str] = {
 
            `machete.gitlab.mrDescriptionIntroStyle` (`create-mr`, `restack-mr` and `retarget-mr`):
 
-                 Select the style of the intro prepended to MR description:
+                 Select the style of the generated section ("intro") added to the MR description:
 
                  * `full`    — include both a chain of upstream MRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream MRs
 
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 1fad397..3ffb8ee 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -907,23 +907,33 @@ class GitContext:
         return result
 
     def __get_patch_id_for_diff(self, patch_contents: str) -> Optional[FullPatchId]:
-        out = utils.get_non_empty_lines(self._popen_git("patch-id", input=patch_contents).stdout)
+        lines = utils.get_non_empty_lines(self._popen_git("patch-id", input=patch_contents).stdout)
 
-        if len(out) == 0:
-            # Line uncovered as we actually always pass a non-empty patch to this method.
+        if len(lines) == 0:
+            # Line uncovered by tests as we actually always pass a non-empty patch to this method.
             return None
-        return FullPatchId.of(out[0].split(' ')[0])  # patch-id output is "<patch-id> <commit-hash>", we only care about the patch-id
+        # The output of patch-id is "<patch-id> <commit-hash>".
+        # Here the bug introduced in git patch-id v2.46.1 (#1329) doesn't affect us as we only care about the patch-id, not commit-hash.
+        return FullPatchId.of(lines[0].split(' ')[0])
 
     def __get_patch_ids_for_commits_between(
             self, earliest_exclusive: AnyRevision, latest_inclusive: AnyRevision, max_commits: int
     ) -> Dict[FullCommitHash, FullPatchId]:
         patches = self._popen_git("log", "--patch", f"^{earliest_exclusive}", latest_inclusive, f"-{max_commits}", "--").stdout
-        patch_ids = self._popen_git("patch-id", input=patches).stdout
+        patch_id_output = self._popen_git("patch-id", input=patches).stdout
 
         patch_id_for_commit: Dict[FullCommitHash, FullPatchId] = {}
-        for line in patch_ids.splitlines():
-            patch_id, commit_hash = line.strip().split(" ", 1)
-            patch_id_for_commit[FullCommitHash.of(commit_hash)] = FullPatchId(patch_id)
+        # TODO (#1329): impose an upper bound on git versions once the underlying issue is fixed
+        if self.get_git_version() <= (2, 46, 0):
+            for line in patch_id_output.splitlines():
+                patch_id, commit_hash = line.strip().split(" ", 1)
+                patch_id_for_commit[FullCommitHash.of(commit_hash)] = FullPatchId(patch_id)
+        else:
+            commit_hashes = [line.replace('commit ', '') for line in patches.splitlines()
+                             if re.fullmatch('commit [0-9a-f]{40}', line)]  # noqa: FS003
+            for line, commit_hash in zip(patch_id_output.splitlines(), commit_hashes):
+                patch_id, _ = line.strip().split(" ", 1)
+                patch_id_for_commit[FullCommitHash.of(commit_hash)] = FullPatchId(patch_id)
 
         return patch_id_for_commit
 
diff --git a/requirements/pypi-deploy.txt b/requirements/pypi-deploy.txt
deleted file mode 100644
index ec0a1e0..0000000
--- a/requirements/pypi-deploy.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-setuptools==74.0.0
-twine==5.1.1
-wheel==0.44.0
diff --git a/requirements/pypi-publish.txt b/requirements/pypi-publish.txt
new file mode 100644
index 0000000..6a4d6d5
--- /dev/null
+++ b/requirements/pypi-publish.txt
@@ -0,0 +1,4 @@
+build==1.2.2
+setuptools==75.0.0
+twine==5.1.1
+wheel==0.44.0
diff --git a/requirements/typos.txt b/requirements/typos.txt
index d609145..02c6584 100644
--- a/requirements/typos.txt
+++ b/requirements/typos.txt
@@ -1,1 +1,1 @@
-typos==1.24.3
+typos==1.24.5
diff --git a/tox.ini b/tox.ini
index 6e81fde..ae93af5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -17,8 +17,6 @@ install_command =
 deps =
   -r{[requirements]dir}/testenv.txt
 allowlist_externals = mkdir
-setenv =
-  PYTHONWARNINGS=ignore:datetime.datetime.utcfromtimestamp() is deprecated
 commands =
   mkdir -p test-results/
   # `python -m pytest` so that the local plugin can be loaded, see https://stackoverflow.com/a/48306599
@@ -26,8 +24,6 @@ commands =
 
 [testenv:test-completions]
 description = "Test shell completions"
-setenv =
-  PYTHONWARNINGS=ignore:easy_install command is deprecated,ignore:setup.py install is deprecated
 commands =
   pip install -e .  # for some reason, `usedevelop = True` seems to be ignored
   mkdir -p test-results/


Test patch:
diff --git a/ci/deb-ppa-test-install/docker-compose.yml b/ci/deb-ppa-test-install/docker-compose.yml
index 0747cfe..9b84888 100644
--- a/ci/deb-ppa-test-install/docker-compose.yml
+++ b/ci/deb-ppa-test-install/docker-compose.yml
@@ -1,7 +1,7 @@
-version: '3'
 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
diff --git a/requirements/testenv.txt b/requirements/testenv.txt
index 321a462..831193c 100644
--- a/requirements/testenv.txt
+++ b/requirements/testenv.txt
@@ -1,6 +1,6 @@
 pytest==7.0.1; python_version <= '3.6'
 pytest==7.4.4; python_version >= '3.7' and python_version < '3.8'
-pytest==8.3.2; python_version >= '3.8'
+pytest==8.3.3; python_version >= '3.8'
 
 pytest-clarity==1.0.1
 
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index db3ef23..1e6b240 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -118,7 +118,8 @@ class TestGitHubCreatePR(BaseTest):
 
         self.repo_sandbox.write_to_file(".git/info/milestone", "42")
         self.repo_sandbox.write_to_file(".git/info/reviewers", "foo\n\nbar")
-        self.repo_sandbox.write_to_file(".github/pull_request_template.md", "# PR title\n## Summary\n## Test plan\n")
+        template = "# PR title\n## Summary\n## Test plan\n\n<!-- start git-machete generated -->\n<!-- end git-machete generated -->\n"
+        self.repo_sandbox.write_to_file(".github/pull_request_template.md", template)
         assert_success(
             ["github", "create-pr", "--draft"],
             """
@@ -155,6 +156,10 @@ class TestGitHubCreatePR(BaseTest):
         assert pr is not None
         assert pr['title'] == 'remove outdated fields'
         assert pr['body'] == textwrap.dedent('''
+            # PR title
+            ## Summary
+            ## Test plan
+
             <!-- start git-machete generated -->
 
             # Based on PR #3
@@ -167,12 +172,7 @@ class TestGitHubCreatePR(BaseTest):
               * **PR #5 (THIS ONE)**:
                 `ignore-trailing` ← `chore/fields`
 
-            <!-- end git-machete generated -->
-
-            # PR title
-            ## Summary
-            ## Test plan
-        ''')[1:]
+            <!-- end git-machete generated -->''')[1:]
         assert pr['draft'] is True
         assert pr['milestone'] == '42'
         assert pr['assignees'] == ['github_user']
@@ -407,8 +407,7 @@ class TestGitHubCreatePR(BaseTest):
 
             # PR title
             ## Summary
-            ## Test plan
-        ''')[1:]
+            ## Test plan''')[1:]
 
     @staticmethod
     def github_api_state_for_test_create_pr_missing_base_branch_on_remote() -> MockGitHubAPIState:
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index 78ac32c..ea7e18d 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -273,6 +273,7 @@ class TestGitHubRetargetPR(BaseTest):
                 `feature` ← `feature_1`
 
             <!-- end git-machete generated -->
+
             # Summary''')[1:]
 
         # branch feature_2 is not present in any of the remotes
@@ -357,6 +358,7 @@ class TestGitHubRetargetPR(BaseTest):
                   `feature_2` ← `feature_3`
 
             <!-- end git-machete generated -->
+
             # Summary''')[1:]
 
         body = \
@@ -393,6 +395,7 @@ class TestGitHubRetargetPR(BaseTest):
                 `feature` ← `feature_3`
 
             <!-- end git-machete generated -->
+
             # Summary''')[1:]
 
         body = \
@@ -447,6 +450,7 @@ class TestGitHubRetargetPR(BaseTest):
                   `feature` ← `feature_4`
 
             <!-- end git-machete generated -->
+
             # Summary''')[1:]
 
     @staticmethod
diff --git a/tests/test_gitlab_create_mr.py b/tests/test_gitlab_create_mr.py
index 4578ea4..30e8d23 100644
--- a/tests/test_gitlab_create_mr.py
+++ b/tests/test_gitlab_create_mr.py
@@ -117,7 +117,8 @@ class TestGitLabCreateMR(BaseTest):
 
         self.repo_sandbox.write_to_file(".git/info/milestone", "42")
         self.repo_sandbox.write_to_file(".git/info/reviewers", "foo\n\nbar")
-        self.repo_sandbox.write_to_file(".gitlab/merge_request_templates/Default.md", "# MR title\n## Summary\n## Test plan\n")
+        template = "# MR title\n## Summary\n## Test plan\n\n<!-- start git-machete generated -->\n<!-- end git-machete generated -->\n"
+        self.repo_sandbox.write_to_file(".gitlab/merge_request_templates/Default.md", template)
         assert_success(
             ["gitlab", "create-mr", "--draft"],
             """
@@ -154,6 +155,10 @@ class TestGitLabCreateMR(BaseTest):
         assert pr is not None
         assert pr['title'] == 'Draft: remove outdated fields'
         assert pr['description'] == textwrap.dedent('''
+            # MR title
+            ## Summary
+            ## Test plan
+
             <!-- start git-machete generated -->
 
             # Based on MR !3
@@ -166,12 +171,7 @@ class TestGitLabCreateMR(BaseTest):
               * **MR !5 (THIS ONE)**:
                 `ignore-trailing` ← `chore/fields`
 
-            <!-- end git-machete generated -->
-
-            # MR title
-            ## Summary
-            ## Test plan
-        ''')[1:]
+            <!-- end git-machete generated -->''')[1:]
         assert pr['milestone_id'] == '42'
         assert pr['assignee_ids'] == [123456]
         assert pr['reviewer_ids'] == [123, 456]
@@ -387,8 +387,7 @@ class TestGitLabCreateMR(BaseTest):
 
             # MR title
             ## Summary
-            ## Test plan
-        ''')[1:]
+            ## Test plan''')[1:]
 
     @staticmethod
     def gitlab_api_state_for_test_create_mr_missing_base_branch_on_remote() -> MockGitLabAPIState:
diff --git a/tests/test_gitlab_retarget_mr.py b/tests/test_gitlab_retarget_mr.py
index c85945c..0d218de 100644
--- a/tests/test_gitlab_retarget_mr.py
+++ b/tests/test_gitlab_retarget_mr.py
@@ -279,6 +279,7 @@ class TestGitLabRetargetMR(BaseTest):
                 `feature` ← `feature_1`
 
             <!-- end git-machete generated -->
+
             # Summary''')[1:]
 
         # branch feature_2 is not present in any of the remotes
@@ -328,7 +329,8 @@ class TestGitLabRetargetMR(BaseTest):
               * **MR !25 (THIS ONE)**:
                 `feature` ← `feature_2`
 
-            <!-- end git-machete generated -->''')[1:]
+            <!-- end git-machete generated -->
+        ''')[1:]
 
         # branch feature_3 present in only one remote: origin_1 and has tracking data
         (
@@ -375,6 +377,7 @@ class TestGitLabRetargetMR(BaseTest):
                   `feature_2` ← `feature_3`
 
             <!-- end git-machete generated -->
+
             # Summary''')[1:]
 
         body = \
@@ -411,6 +414,7 @@ class TestGitLabRetargetMR(BaseTest):
                 `feature` ← `feature_3`
 
             <!-- end git-machete generated -->
+
             # Summary''')[1:]
 
         body = \