Repository: VirtusLab/git-machete
Pull request #1192 (2024-03-08 23:47:23)

Release v3.24.0


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 55376ca..5262a81 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -3,21 +3,25 @@ version: 2.1
 executors:
   macos_executor:
     macos:
-      xcode: 14.3.1
+      # https://circleci.com/docs/using-macos/#supported-xcode-versions-intel
+      xcode: 15.3.0
+    # https://circleci.com/docs/using-macos/#available-resource-classes
+    # As of Feb 2024, M1 runners are unavailable in free tier :/
     resource_class: macos.x86.medium.gen2
   ubuntu_executor:
     machine:
-      image: ubuntu-2204:2022.10.2
+      # https://circleci.com/developer/images?imageType=machine
+      image: ubuntu-2204:2024.01.1
     environment:
       # Needed to avoid warning: https://superuser.com/questions/575844/warning-terminal-is-not-fully-functional
       TERM: ansi
     resource_class: large
   windows_executor:
     machine:
-      image: windows-server-2022-gui:2023.03.1
+      # https://circleci.com/developer/images?imageType=machine
+      image: windows-server-2022-gui:2023.11.1
       shell: bash.exe
-    # Jobs using windows.large sometimes get stuck on `Preparing environment` :/
-    resource_class: windows.medium
+    resource_class: windows.large
 
 orbs:
   codecov: codecov/codecov@4.0.1
diff --git a/README.md b/README.md
index 0f71a43..a69486b 100644
--- a/README.md
+++ b/README.md
@@ -13,11 +13,10 @@
 [![CircleCI](https://circleci.com/gh/VirtusLab/git-machete/tree/master.svg?style=shield)](https://app.circleci.com/pipelines/github/VirtusLab/git-machete?branch=master)
 [![codecov](https://codecov.io/gh/VirtusLab/git-machete/branch/develop/graph/badge.svg)](https://codecov.io/gh/VirtusLab/git-machete)
 
-[//]: # (The images are referenced by full URLs to ensure they render correctly on https://pypi.org/project/git-machete/)
-<img src="https://raw.githubusercontent.com/VirtusLab/git-machete/master/graphics/logo/svg/with-name.svg#gh-light-mode-only"
-     style="width: 100%; display: block; margin-bottom: 10pt;" />
-<img src="https://raw.githubusercontent.com/VirtusLab/git-machete/master/graphics/logo/svg/with-name-dark.svg#gh-dark-mode-only"
-     style="width: 100%; display: block; margin-bottom: 10pt;" />
+[//]: # (These images are referenced by full URLs to ensure they render correctly on https://pypi.org/project/git-machete/)
+[//]: # (In fact, only the light-mode image is used in PyPI, the other one is cropped out when publishing the package. Still, using the same format for consistency)
+<img src="https://raw.githubusercontent.com/VirtusLab/git-machete/master/graphics/logo/svg/with-name.svg#gh-light-mode-only"     style="width: 100%; display: block; margin-bottom: 10pt;" />
+<img src="https://raw.githubusercontent.com/VirtusLab/git-machete/master/graphics/logo/svg/with-name-dark.svg#gh-dark-mode-only" style="width: 100%; display: block; margin-bottom: 10pt;" />
 
 💪 git-machete is a robust tool that **simplifies your git workflows**.<br/>
 
@@ -55,26 +54,21 @@ git-machete requires Python >= 3.6. Python 2.x is no longer supported.
 brew install git-machete
 ```
 
-### Using pip with sudo (system-wide install)
+### Using pip
 
 You need to have Python and `pip` installed from system packages.
 
+**For user-wide install:**
 ```shell script
-sudo -H pip install git-machete
+pip install --user git-machete
 ```
+Please verify that your `PATH` variable has `${HOME}/.local/bin/` included.
 
-**Tip:** pass an extra `-U` flag to `pip install` to upgrade an already installed version.
-
-### Using pip without sudo (user-wide install)
-
-You need to have Python and `pip` installed from system packages.
-
+**For system-wide install:**
 ```shell script
-pip install --user git-machete
+sudo -H pip install git-machete  # system-wide install
 ```
 
-Please verify that your `PATH` variable has `${HOME}/.local/bin/` included.
-
 **Tip:** pass an extra `-U` flag to `pip install` to upgrade an already installed version.
 
 ### Using conda
@@ -93,7 +87,7 @@ sudo snap install --classic git-machete
 
 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).
+**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)
 
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index ab6ae38..d213593 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,10 @@
 # Release notes
 
+## New in git-machete 3.24.0
+
+- added: `-f`/`--as-first-child` flag to `git machete add` (contributed by @matthalp)
+- fixed: `git machete github retarget-pr` not updating description of PR due to stray `\r` characters
+
 ## New in git-machete 3.23.2
 
 - fixed: make fork-point also take into account common ancestors (and not only reflogs) in more cases
diff --git a/ci/checks/enforce-bumped-version.sh b/ci/checks/enforce-bumped-version.sh
index ad45ba5..64103f0 100755
--- a/ci/checks/enforce-bumped-version.sh
+++ b/ci/checks/enforce-bumped-version.sh
@@ -4,7 +4,8 @@ set -e -o pipefail -u
 
 last_tag=$(git describe --tags --abbrev=0)
 last_tag_version=${last_tag/v/}
-current_version=$(python3 setup.py --version)
+# Not using `python3 setup.py --version` to make it work in the contexts where setuptools are missing (Python 3.12+)
+current_version=$(cut -d\' -f2 git_machete/__init__.py)
 
 if [[ "$(echo -e "$current_version\n$last_tag_version" | sort --version-sort | tail -n 1)" == "$last_tag_version" ]]; then
   echo "Please increment version in git_machete/__init__.py with a new release! Last released version: $last_tag_version"
diff --git a/ci/checks/enforce-release-notes-up-to-date.sh b/ci/checks/enforce-release-notes-up-to-date.sh
index 47d7806..3cdc084 100755
--- a/ci/checks/enforce-release-notes-up-to-date.sh
+++ b/ci/checks/enforce-release-notes-up-to-date.sh
@@ -2,7 +2,8 @@
 
 set -e -o pipefail -u
 
-current_version=$(python3 setup.py --version)
+# Not using `python3 setup.py --version` to make it work in the contexts where setuptools are missing (Python 3.12+)
+current_version=$(cut -d\' -f2 git_machete/__init__.py)
 release_notes_version=$(sed '3!d' RELEASE_NOTES.md | grep -Eo '(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))+')
 
 if [[ $current_version != "$release_notes_version" ]]; then
diff --git a/ci/checks/prohibit-split-backslash-n.sh b/ci/checks/prohibit-split-backslash-n.sh
new file mode 100755
index 0000000..d4655cd
--- /dev/null
+++ b/ci/checks/prohibit-split-backslash-n.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
+source "$self_dir"/utils.sh
+
+if git grep -n 'split(.\\n.)' -- '*.py'; then
+  die 'Do not use split("\\n") on strings as it does not cover \\r\\n line breaks; use splitlines() instead'
+fi
diff --git a/ci/checks/run-all-checks.sh b/ci/checks/run-all-checks.sh
index 536aedd..6a0a46d 100755
--- a/ci/checks/run-all-checks.sh
+++ b/ci/checks/run-all-checks.sh
@@ -31,6 +31,7 @@ prohibit-id-est-in-rst.sh
 prohibit-double-backticks-in-python.sh
 prohibit-markdown-links-in-rst.sh
 prohibit-single-backtick-in-rst.sh
+prohibit-split-backslash-n.sh
 prohibit-strings-split-without-delimiter.sh
 prohibit-strings-with-backslash-continuation.sh
 prohibit-strings-with-useless-interpolations.sh
diff --git a/ci/homebrew/ci-deploy.sh b/ci/homebrew/ci-deploy.sh
index 81553ce..5cf57e0 100644
--- a/ci/homebrew/ci-deploy.sh
+++ b/ci/homebrew/ci-deploy.sh
@@ -2,14 +2,14 @@
 
 set -e -o pipefail -u -x
 
-HOMEBREW_VERSION=4.1.21
+HOMEBREW_VERSION=4.2.11
 
 export HOMEBREW_NO_AUTO_UPDATE=1
 
 NONINTERACTIVE=1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
 # https://stackoverflow.com/questions/54912857/how-do-i-install-old-version-of-homebrew-itself-not-the-formula
 (cd /home/linuxbrew/.linuxbrew/Homebrew; git checkout $HOMEBREW_VERSION)
-## The two lines below are added to avoid ->  Warning: /home/linuxbrew/.linuxbrew/bin is not in your PATH
+# The two lines below are added to avoid ->  Warning: /home/linuxbrew/.linuxbrew/bin is not in your PATH
 (echo; echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"') >> /home/circleci/.bash_profile
 eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
 brew --version
@@ -28,45 +28,31 @@ sha256=$(
   curl -s https://$pypi_host/pypi/git-machete/"$version"/json \
   | jq --raw-output '.urls | map(select(.packagetype == "sdist")) | .[0].digests.sha256')
 
-git config --global user.email "gitmachete@virtuslab.com"
-git config --global user.name "Git Machete Bot"
-
-# Relying on HOMEBREW_GITHUB_API_TOKEN, provided by the CI
-# See https://docs.brew.sh/Manpage -> Ctrl+F HOMEBREW_GITHUB_API_TOKEN
-
-# We need to run `brew tap homebrew/core` manually because since Homebrew 4.0.0 it is no longer done by default when installing `brew`; see https://brew.sh/2023/02/16/homebrew-4.0.0/
-brew tap homebrew/core
 echo "Bump Homebrew formula"
-# `--force` ignores the existence of open PRs for the same formula.
-# It is useful for the rare cases where a develop/master build runs while a PR for the previously released version is still pending.
-# See https://app.circleci.com/pipelines/github/VirtusLab/git-machete/3140/workflows/6ee0916d-fc11-49b2-b29a-5bbc95cb25c4/jobs/16418:
-#   Error: These open pull requests may be duplicates:
-#   git-machete 3.15.1 https://github.com/Homebrew/homebrew-core/pull/123123
-#   Duplicate PRs should not be opened. Use --force to override this error.
-flags=(--force --no-browse --verbose --url "$url" --sha256 "$sha256")
+brew developer on
 if [[ $do_push == true ]]; then
-  brew bump-formula-pr "${flags[@]}" git-machete
+  # Relying on HOMEBREW_GITHUB_API_TOKEN, provided by the CI
+  # See https://docs.brew.sh/Manpage -> Ctrl+F HOMEBREW_GITHUB_API_TOKEN
+
+  git config --global user.email "gitmachete@virtuslab.com"
+  git config --global user.name "Git Machete Bot"
+
+  # `--force` ignores the existence of open PRs for the same formula.
+  # It is useful for the rare cases where a develop/master build runs while a PR for the previously released version is still pending.
+  # See https://app.circleci.com/pipelines/github/VirtusLab/git-machete/3140/workflows/6ee0916d-fc11-49b2-b29a-5bbc95cb25c4/jobs/16418:
+  #   Error: These open pull requests may be duplicates:
+  #   git-machete 3.15.1 https://github.com/Homebrew/homebrew-core/pull/123123
+  #   Duplicate PRs should not be opened. Use --force to override this error.
+  brew bump-formula-pr --force --no-browse --verbose --url "$url" --sha256 "$sha256" git-machete
 else
   echo "Refraining from push since it's a dry run"
-  brew bump-formula-pr --write-only "${flags[@]}" git-machete
+  git clone --depth=1 git@github.com:Homebrew/homebrew-core.git ~/homebrew-core
+  formula_file=~/homebrew-core/Formula/g/git-machete.rb
+  sed -i "s!^  url .*\$!  url \"$url\"!"          $formula_file
+  sed -i "s!^  sha256 .*\$!  sha256 \"$sha256\"!" $formula_file
 
-  export HOMEBREW_NO_INSTALL_FROM_API=1
   brew config
-  echo "Attempt to install the formula locally"
-  attempts=5
-  i=1
-  while true; do
-    if brew install --build-from-source --formula /home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/g/git-machete.rb; then
-      break
-    elif (( i < attempts )); then
-      echo "Retrying the installation..."
-      i=$((i + 1))
-      sleep 60
-    else
-      echo "Installing the formula locally did not succeed despite $attempts attempts"
-      exit 1
-    fi
-  done
+  brew install --build-from-source --formula $formula_file
 
   if [[ "$version" != "$(git machete version | cut -d' ' -f3)" ]]; then
     echo "Something went wrong during brew installation: installed version does not match version from formula."
diff --git a/completion/git-machete.completion.bash b/completion/git-machete.completion.bash
index 01385ab..fa480c7 100644
--- a/completion/git-machete.completion.bash
+++ b/completion/git-machete.completion.bash
@@ -14,7 +14,7 @@ _git_machete() {
   local shells="bash fish zsh"
 
   local common_opts="--debug -h --help -v --verbose"
-  local add_opts="-o --onto= -R --as-root -y --yes"
+  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 --sync-github-prs"
   local delete_unmanaged_opts="-y --yes"
diff --git a/completion/git-machete.completion.zsh b/completion/git-machete.completion.zsh
index 7ed3c32..9b5de22 100644
--- a/completion/git-machete.completion.zsh
+++ b/completion/git-machete.completion.zsh
@@ -19,8 +19,9 @@ _git-machete() {
         (add)
           _arguments \
             '1:: :__git_machete_list_addable' \
-            '(-o --onto)'{-o,--onto=}'[Specify the target parent branch to add the given branch onto]: :__git_machete_list_managed' \
+            '(-f --as-first-child)'{-f,--as-first-child}'[Add the given branch as the first (instead of last) child of its parent]' \
             '(-R --as-root)'{-R,--as-root}'[Add the given branch as a new root]' \
+            '(-o --onto)'{-o,--onto=}'[Specify the target parent branch to add the given branch onto]: :__git_machete_list_managed' \
             '(-y --yes)'{-y,--yes}'[Do not ask for confirmation whether to create the branch or whether to add onto the inferred upstream]' \
             "${common_flags[@]}"
           ;;
diff --git a/completion/git-machete.fish b/completion/git-machete.fish
index 880066b..6858b58 100644
--- a/completion/git-machete.fish
+++ b/completion/git-machete.fish
@@ -34,11 +34,12 @@ function __machete_with_overridden_fork_point_branches
 end
 
 # git machete add
-complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands"                                         -f                 -a add                            -d 'Add a branch to the tree of branch dependencies'
-complete -c git-machete -n "__fish_seen_subcommand_from add"                                                             -f                 -a '(__machete_addable_branches)'
-complete -c git-machete -n "__fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --as-root -R"           -x -l onto    -s o -a '(__machete_managed_branches)' -d 'Specifies the target parent branch to add the given branch onto. Cannot be specified together with -R/--as-root'
-complete -c git-machete -n "__fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --onto -o --as-root -R" -f -l as-root -s R                                   -d 'Add the given branch as a new root (and not onto any other branch). Cannot be specified together with -o/--onto'
-complete -c git-machete -n "__fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --yes -y"               -f -l yes     -s y                                   -d 'Do not ask for confirmation whether to create the branch or whether to add onto the inferred upstream'
+complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands"                                                   -f                        -a add                            -d 'Add a branch to the tree of branch dependencies'
+complete -c git-machete -n "__fish_seen_subcommand_from add"                                                                       -f                        -a '(__machete_addable_branches)'
+complete -c git-machete -n "__fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --as-first-child -f --as-root -R" -f -l as-first-child -s f                                   -d 'Add the given branch as the first (instead of last) child of its parent'
+complete -c git-machete -n "__fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --as-root -R"                     -x -l onto           -s o -a '(__machete_managed_branches)' -d 'Specifies the target parent branch to add the given branch onto. Cannot be specified together with -R/--as-root'
+complete -c git-machete -n "__fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --onto -o --as-root -R"           -f -l as-root        -s R                                   -d 'Add the given branch as a new root (and not onto any other branch). Cannot be specified together with -o/--onto'
+complete -c git-machete -n "__fish_seen_subcommand_from add; and not __fish_seen_subcommand_from --yes -y"                         -f -l yes            -s y                                   -d 'Do not ask for confirmation whether to create the branch or whether to add onto the inferred upstream'
 
 # git machete advance
 complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands"                               -f -a advance  -d 'Fast-forward merge one of children to the current branch and then slide out this child'
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 24f6a6e..119b629 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" "Feb 08, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Feb 26, 2024" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.23.2
+git-machete \- git-machete 3.24.0
 .sp
 git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
 .sp
@@ -152,6 +152,10 @@ Note: all the effects of \fBadd\fP (except git branch creation) can as well be a
 \fBOptions:\fP
 .INDENT 0.0
 .TP
+.B  \-f\fP,\fB  \-\-as\-first\-child
+Add the given branch as the first (instead of last) child of its parent.
+Cannot be specified together with \fB\-R/\-\-as\-root\fP\&.
+.TP
 .BI \-o\fP,\fB  \-\-onto\fB= <target\-upstream\-branch>
 Specifies the target parent branch to add the given branch onto.
 Cannot be specified together with \fB\-R/\-\-as\-root\fP\&.
diff --git a/docs/source/cli/add.rst b/docs/source/cli/add.rst
index 15fbcdd..268f1a5 100644
--- a/docs/source/cli/add.rst
+++ b/docs/source/cli/add.rst
@@ -24,6 +24,9 @@ Note: all the effects of ``add`` (except git branch creation) can as well be ach
 
 **Options:**
 
+-f, --as-first-child                   Add the given branch as the first (instead of last) child of its parent.
+                                       Cannot be specified together with ``-R/--as-root``.
+
 -o, --onto=<target-upstream-branch>    Specifies the target parent branch to add the given branch onto.
                                        Cannot be specified together with ``-R/--as-root``.
 
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 77882e2..6c369f4 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.23.2'
+__version__ = '3.24.0'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 81fd8f3..83f95c5 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -143,6 +143,7 @@ def create_cli_parser() -> argparse.ArgumentParser:
         add_help=False,
         parents=[common_args_parser])
     add_parser.add_argument('branch', nargs='?')
+    add_parser.add_argument('-f', '--as-first-child', action='store_true')
     add_parser.add_argument('-o', '--onto')
     add_parser.add_argument('-R', '--as-root', action='store_true')
     add_parser.add_argument('-y', '--yes', action='store_true')
@@ -413,7 +414,9 @@ def update_cli_options_using_parsed_args(
 
     for opt, arg in vars(parsed_args).items():
         # --color, --debug and --verbose are handled outside this method
-        if opt == "as_root":
+        if opt == "as_first_child":
+            cli_opts.opt_as_first_child = True
+        elif opt == "as_root":
             cli_opts.opt_as_root = True
         elif opt == "branch":
             cli_opts.opt_branch = AnyBranchName.of(arg) if arg else None
@@ -601,6 +604,7 @@ def launch(orig_args: List[str]) -> None:
             machete_client.add(
                 branch=branch,
                 opt_onto=cli_opts.opt_onto,
+                opt_as_first_child=cli_opts.opt_as_first_child,
                 opt_as_root=cli_opts.opt_as_root,
                 opt_yes=cli_opts.opt_yes,
                 verbose=True,
diff --git a/git_machete/client.py b/git_machete/client.py
index 2b2ec1d..4aa6415 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -232,6 +232,7 @@ class MacheteClient:
             *,
             branch: LocalBranchShortName,
             opt_onto: Optional[LocalBranchShortName],
+            opt_as_first_child: bool,
             opt_as_root: bool,
             opt_yes: bool,
             verbose: bool,
@@ -317,10 +318,12 @@ class MacheteClient:
 
             self.__up_branch[branch] = opt_onto
 
-            if opt_onto in self.__down_branches:
-                self.__down_branches[opt_onto] += [branch]
+            existing_down_branches = self.__down_branches[opt_onto] if opt_onto in self.__down_branches else []
+            if opt_as_first_child:
+                down_branches = [branch] + existing_down_branches
             else:
-                self.__down_branches[opt_onto] = [branch]
+                down_branches = existing_down_branches + [branch]
+            self.__down_branches[opt_onto] = down_branches
             if verbose:
                 print(fmt(f"Added branch {bold(branch)} onto {bold(opt_onto)}"))
 
@@ -2112,6 +2115,7 @@ class MacheteClient:
             if reversed_path[0].base not in self.managed_branches:
                 self.add(
                     branch=LocalBranchShortName.of(reversed_path[0].base),
+                    opt_as_first_child=False,
                     opt_as_root=True,
                     opt_onto=None,
                     opt_yes=True,
@@ -2122,6 +2126,7 @@ class MacheteClient:
                     self.add(
                         branch=LocalBranchShortName.of(pr_on_path.head),
                         opt_onto=LocalBranchShortName.of(pr_on_path.base),
+                        opt_as_first_child=False,
                         opt_as_root=False,
                         opt_yes=True,
                         verbose=False,
@@ -2303,19 +2308,13 @@ class MacheteClient:
         def skip_leading_empty(strs: List[str]) -> List[str]:
             return list(itertools.dropwhile(lambda line: line.strip() == '', strs))
 
-        lines = skip_leading_empty(old_description.split('\n')) if old_description else []
+        lines = skip_leading_empty(old_description.splitlines()) if old_description else []
         text_to_prepend = self.__generate_text_to_prepend_to_pr_description(github_client, pr)
-        lines_to_prepend = text_to_prepend.split('\n') if text_to_prepend else []
+        lines_to_prepend = text_to_prepend.splitlines() if text_to_prepend else []
         if self.START_GIT_MACHETE_GENERATED_COMMENT in lines and self.END_GIT_MACHETE_GENERATED_COMMENT in lines:
             start_index = lines.index(self.START_GIT_MACHETE_GENERATED_COMMENT)
             end_index = lines.index(self.END_GIT_MACHETE_GENERATED_COMMENT)
-            if lines_to_prepend == []:
-                if lines[end_index + 1:] and lines[end_index + 1] == '':
-                    lines = lines[:start_index] + lines[end_index + 2:]
-                else:
-                    lines = lines[:start_index] + lines[end_index + 1:]
-            else:
-                lines = lines[:start_index] + lines_to_prepend[:-1] + lines[end_index + 1:]
+            lines = lines[:start_index] + lines_to_prepend + lines[end_index + 1:]
         else:
             # For compatibility with pre-v3.23.0 format
             if lines and '# Based on PR #' in lines[0]:
@@ -2743,6 +2742,7 @@ class MacheteClient:
         if current_branch not in self.managed_branches:
             self.add(branch=current_branch,
                      opt_onto=opt_onto,
+                     opt_as_first_child=False,
                      opt_as_root=False,
                      opt_yes=opt_yes,
                      verbose=True,
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index f84553c..ec16b25 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -54,6 +54,9 @@ long_docs: Dict[str, str] = {
         Note: all the effects of `add` (except git branch creation) can as well be achieved by manually editing the branch layout file.
 
         <b>Options:</b>
+           <b>-f</b>, <b>--as-first-child</b>
+              Add the given branch as the first (instead of last) child of its parent.
+              Cannot be specified together with `-R/--as-root`.
            <b>-o</b>, <b>--onto=<target-upstream-branch></b>
               Specifies the target parent branch to add the given branch onto.
               Cannot be specified together with `-R/--as-root`.
diff --git a/git_machete/options.py b/git_machete/options.py
index 49347e1..496c8b4 100644
--- a/git_machete/options.py
+++ b/git_machete/options.py
@@ -7,6 +7,7 @@ from .git_operations import AnyBranchName, AnyRevision, LocalBranchShortName
 class CommandLineOptions:
 
     def __init__(self) -> None:
+        self.opt_as_first_child: bool = False
         self.opt_as_root: bool = False
         self.opt_branch: Optional[AnyBranchName] = None
         self.opt_checked_out_since: Optional[str] = None
@@ -44,6 +45,9 @@ class CommandLineOptions:
         if self.opt_as_root and self.opt_onto:
             raise MacheteException(
                 "Option `-R/--as-root` cannot be specified together with `-o/--onto`.")
+        if self.opt_as_root and self.opt_as_first_child:
+            raise MacheteException(
+                "Option `-R/--as-root` cannot be specified together with `-f/--as-first-child`.")
         if self.opt_no_edit_merge and not self.opt_merge:
             raise MacheteException(
                 "Option `--no-edit-merge` only makes sense when using merge and "
diff --git a/git_machete/utils.py b/git_machete/utils.py
index 6c40769..c5ee54f 100644
--- a/git_machete/utils.py
+++ b/git_machete/utils.py
@@ -45,7 +45,7 @@ def map_truthy_only(func: Callable[[T], Optional[U]], iterable: Iterable[T]) ->
 
 
 def get_non_empty_lines(s: str) -> List[str]:
-    return list(filter(None, s.split("\n")))
+    return list(filter(None, s.splitlines()))
 
 
 # Converts a lambda accepting N arguments to a lambda accepting one argument, an N-element tuple.
diff --git a/requirements/coverage.txt b/requirements/coverage.txt
index f2f736d..58228df 100644
--- a/requirements/coverage.txt
+++ b/requirements/coverage.txt
@@ -1,10 +1,10 @@
 coverage==6.2; python_version <= '3.6'
 coverage==7.2.7; python_version >= '3.7' and python_version < '3.8'
-coverage==7.4.1; python_version >= '3.8'
+coverage==7.4.3; python_version >= '3.8'
 
 pytest==7.0.1; python_version <= '3.6'
 pytest==7.4.4; python_version >= '3.7' and python_version < '3.8'
-pytest==8.0.1; python_version >= '3.8'
+pytest==8.1.0; python_version >= '3.8'
 
 pytest-mock==3.6.1; python_version <= '3.6'
 pytest-mock==3.11.1; python_version >= '3.7' and python_version < '3.8'
diff --git a/requirements/isort.txt b/requirements/isort.txt
index 86153d1..b191d41 100644
--- a/requirements/isort.txt
+++ b/requirements/isort.txt
@@ -1,2 +1,2 @@
-autoflake==2.2.1
+autoflake==2.3.0
 isort==5.13.2
diff --git a/requirements/mypy.txt b/requirements/mypy.txt
index 03da77e..5d9d870 100644
--- a/requirements/mypy.txt
+++ b/requirements/mypy.txt
@@ -4,7 +4,7 @@ mypy==1.8.0; python_version >= '3.8'
 
 pytest==7.0.1; python_version <= '3.6'
 pytest==7.4.4; python_version >= '3.7' and python_version < '3.8'
-pytest==8.0.1; python_version >= '3.8'
+pytest==8.1.0; python_version >= '3.8'
 
 pytest-mock==3.6.1; python_version <= '3.6'
 pytest-mock==3.11.1; python_version >= '3.7' and python_version < '3.8'
diff --git a/requirements/pypi-deploy.txt b/requirements/pypi-deploy.txt
index 067eb17..14f2d79 100644
--- a/requirements/pypi-deploy.txt
+++ b/requirements/pypi-deploy.txt
@@ -1,2 +1,3 @@
+setuptools==69.1.1
 twine==5.0.0
 wheel==0.42.0
diff --git a/requirements/typos.txt b/requirements/typos.txt
index c7358af..5087601 100644
--- a/requirements/typos.txt
+++ b/requirements/typos.txt
@@ -1,1 +1,1 @@
-typos==1.18.2
+typos==1.19.0
diff --git a/setup.py b/setup.py
index f8fb196..9a85122 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 
+import re
 from os import path
 
 from setuptools import setup
@@ -8,7 +9,8 @@ from git_machete import __version__
 
 this_directory = path.abspath(path.dirname(__file__))
 with open(path.join(this_directory, 'README.md'), mode="r", encoding="utf-8") as f:
-    long_description = f.read()
+    # PyPI webpage seems to be always served with white background, let's remove the GitHub-specific dark variants of images.
+    long_description = re.sub("^.*#gh-dark-mode-only.*$", "", f.read())
 
 setup(
     name='git-machete',
diff --git a/tox.ini b/tox.ini
index 5b7faf0..2b05813 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,7 +26,7 @@ description = "Test shell completions"
 setenv =
   PYTHONWARNINGS=ignore:easy_install command is deprecated,ignore:setup.py install is deprecated
 commands =
-  python setup.py develop  # for some reason, `usedevelop = True` seems to be ignored
+  pip install -e .  # for some reason, `usedevelop = True` seems to be ignored
   mkdir -p test-results/
   pytest --numprocesses=auto --junitxml=test-results/testenv-{envname}.xml -m completion_e2e {posargs}
 


Test patch:
diff --git a/requirements/testenv.txt b/requirements/testenv.txt
index 9935976..0602bc2 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.0.1; python_version >= '3.8'
+pytest==8.1.0; python_version >= '3.8'
 
 pytest-mock==3.6.1; python_version <= '3.6'
 pytest-mock==3.11.1; python_version >= '3.7' and python_version < '3.8'
diff --git a/tests/completion_e2e/test_completion_e2e.py b/tests/completion_e2e/test_completion_e2e.py
index 90ece35..da4a310 100644
--- a/tests/completion_e2e/test_completion_e2e.py
+++ b/tests/completion_e2e/test_completion_e2e.py
@@ -16,6 +16,8 @@ test_cases: Dict[str, str] = {
         "add advance anno",
     "git machete add ":
         "feature",
+    "git machete add -":
+        "-R --as-first-child --as-root --debug -f -h --help -o --onto -v --verbose -y --yes",
     "git machete add -o ":
         "develop master",
     "git machete add --onto ":
diff --git a/tests/test_add.py b/tests/test_add.py
index 8eb5e4e..bc2f4c3 100644
--- a/tests/test_add.py
+++ b/tests/test_add.py
@@ -10,7 +10,9 @@ class TestAdd(BaseTest):
 
     def test_add(self, mocker: MockerFixture) -> None:
         (
-            self.repo_sandbox.new_branch("master")
+            self.repo_sandbox
+                .remove_remote("origin")
+                .new_branch("master")
                 .commit("master commit.")
                 .new_branch("develop")
                 .commit("develop commit.")
@@ -52,7 +54,7 @@ class TestAdd(BaseTest):
         self.repo_sandbox.check_out('develop')
         self.repo_sandbox.new_branch("bugfix/another_feature")
         assert_success(
-            ['add', '-y', 'refs/heads/bugfix/another_feature'],
+            ['add', '--as-first-child', '-y', 'refs/heads/bugfix/another_feature'],
             'Adding bugfix/another_feature onto the inferred upstream (parent) branch develop\n'
             'Added branch bugfix/another_feature onto develop\n'
         )
@@ -65,6 +67,22 @@ class TestAdd(BaseTest):
             'Added branch chore/remove_indentation onto feature\n'
         )
 
+        assert_success(["status"], """
+            master
+            |
+            o-develop
+              |
+              o-bugfix/another_feature
+              |
+              x-feature
+              | |
+              | x-chore/remove_indentation *
+              |
+              o-bugfix/feature_fail
+              |
+              o-bugfix/some_feature
+        """)
+
     def test_add_check_out_remote_branch(self, mocker: MockerFixture) -> None:
         """
         Verify the behaviour of a 'git machete add' command in the special case when a remote branch is checked out locally.
@@ -232,3 +250,9 @@ class TestAdd(BaseTest):
             ['add', '--onto', 'foo', '--as-root'],
             "Option -R/--as-root cannot be specified together with -o/--onto."
         )
+
+    def test_add_as_root_with_as_first_child(self) -> None:
+        assert_failure(
+            ['add', '--as-first-child', '--as-root'],
+            "Option -R/--as-root cannot be specified together with -f/--as-first-child."
+        )
diff --git a/tests/test_github_restack_pr.py b/tests/test_github_restack_pr.py
index e7b4f64..06e46ba 100644
--- a/tests/test_github_restack_pr.py
+++ b/tests/test_github_restack_pr.py
@@ -120,6 +120,7 @@ class TestGitHubRestackPR(BaseTest):
         assert pr is not None
         assert pr['draft'] is True
         assert pr['base']['ref'] == 'master'
+        assert pr['body'] == '# Summary'
 
     def test_github_restack_pr_branch_diverged_and_newer(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake)
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index 50818c3..b9e01a0 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -274,7 +274,6 @@ class TestGitHubRetargetPR(BaseTest):
             * PR #15: `feature` ➔ `master`
 
             <!-- end git-machete generated -->
-
             # Summary''')[1:]
 
         # branch feature_2 is not present in any of the remotes
@@ -321,8 +320,7 @@ class TestGitHubRetargetPR(BaseTest):
             * PR #25: `feature_2` ➔ `feature`
             * PR #15: `feature` ➔ `master`
 
-            <!-- end git-machete generated -->
-        ''')[1:]
+            <!-- end git-machete generated -->''')[1:]
 
         # branch feature_3 present in only one remote: origin_1 and has tracking data
         (
@@ -364,7 +362,6 @@ class TestGitHubRetargetPR(BaseTest):
             * PR #15: `feature` ➔ `master`
 
             <!-- end git-machete generated -->
-
             # Summary''')[1:]
 
         body = \
@@ -398,7 +395,6 @@ class TestGitHubRetargetPR(BaseTest):
             * PR #15: `feature` ➔ `master`
 
             <!-- end git-machete generated -->
-
             # Summary''')[1:]
 
         body = \