Repository: VirtusLab/git-machete
Pull request #1132 (2023-12-10 14:16:41)

Release v3.22.0


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 24953c6..6f26c60 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -45,7 +45,7 @@ jobs:
 
             sudo apt-get install zsh
 
-            pip3 install tox typos
+            pip3 install tox
 
             # For enforce-shell-scripts-pass-shellcheck.sh
             sudo apt-get install shellcheck
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 32b6d07..a1927bf 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,10 @@
 # Release notes
 
+## New in git-machete 3.22.0
+
+- improved: if neither `.git/info/description` nor `.github/pull_request_template.md` is present, `git machete github create-pr` now uses message body of the first unique commit as PR description (suggested by @kamaradclimber)
+- added: `machete.github.forceDescriptionFromCommitMessage` git config key that forces `git machete github create-pr` to use message body of the first unique commit as PR description (suggested by @kamaradclimber)
+
 ## New in git-machete 3.21.1
 
 - fixed: `Cannot parse Link header` error in `git machete github` subcommands when there are more than 100 PRs in the given repository (reported by @domesticsimian)
diff --git a/blogs/docker-ci-tricks-2/blog.md b/blogs/docker-ci-tricks-2/blog.md
index 11291fa..025b116 100644
--- a/blogs/docker-ci-tricks-2/blog.md
+++ b/blogs/docker-ci-tricks-2/blog.md
@@ -89,7 +89,7 @@ docker-compose pull $image_name || {
 ```
 
 Once we've got the directory hash, we check (`docker-compose pull`) whether the image with the given tag is already present
-in the [virtuslab/git-machete-ci-tox repository on Docker Hub](https://hub.docker.com/r/virtuslab/git-machete-ci-tox/tags).
+in the [virtuslab/git-machete-ci-tox repository on Docker Hub](https://hub.docker.com/r/gitmachete/ci-tox/tags).
 
 If there is no previous build for the given combination of Git version, Python version and contents of ci/tox directory,
 `docker-compose pull` would return a non-zero exit code, although this can also mean a failure of the pull operation.
diff --git a/ci/checks/prohibit-typos.sh b/ci/checks/prohibit-typos.sh
deleted file mode 100755
index 4439ef2..0000000
--- a/ci/checks/prohibit-typos.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-
-# See https://github.com/crate-ci/typos
-typos --ignore --format brief
diff --git a/ci/checks/run-all-checks.sh b/ci/checks/run-all-checks.sh
index 5c3d557..536aedd 100755
--- a/ci/checks/run-all-checks.sh
+++ b/ci/checks/run-all-checks.sh
@@ -36,6 +36,3 @@ prohibit-strings-with-backslash-continuation.sh
 prohibit-strings-with-useless-interpolations.sh
 prohibit-tab-character.sh
 prohibit-trailing-whitespace.sh
-if command -v typos &>/dev/null; then
-  prohibit-typos.sh
-fi
diff --git a/ci/tox/build-context/run.sh b/ci/tox/build-context/run.sh
index f326532..243d290 100644
--- a/ci/tox/build-context/run.sh
+++ b/ci/tox/build-context/run.sh
@@ -22,7 +22,7 @@ if [[ $CHECK_DOCS_UP_TO_DATE = true ]]; then
 fi
 
 if [[ $CHECK_PEP8 = true ]]; then
-  TOX_ENV_LIST="$TOX_ENV_LIST,isort-check,pep8-check,vulture-check"
+  TOX_ENV_LIST="$TOX_ENV_LIST,isort-check,pep8-check,vulture-check,typos-check"
 fi
 
 tox -e "$TOX_ENV_LIST"
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 4505dc7..0f262f5 100644
--- a/docs/man/git-machete.1
+++ b/docs/man/git-machete.1
@@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "GIT-MACHETE" "1" "Dec 07, 2023" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Dec 08, 2023" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.21.1
+git-machete \- git-machete 3.22.0
 .sp
 git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
 .sp
@@ -396,6 +396,10 @@ to not only include PR number and author (if different from the current user), b
 The affected (sub)commands clearly include \fBanno \-\-sync\-github\-prs\fP and \fBgithub anno\-prs\fP,
 but also \fBgithub checkout\-prs\fP, \fBgithub create\-pr\fP, \fBgithub retarget\-pr\fP and \fBgithub restack\-pr\fP\&.
 .TP
+.B \fBmachete.github.forceDescriptionFromCommitMessage\fP:
+Setting this config key to \fBtrue\fP will force \fBgit machete github create\-pr\fP to take PR description
+from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.github/pull_request_template.md\fP is present.
+.TP
 .B \fBmachete.github.{domain,remote,organization,repository}\fP:
 .INDENT 7.0
 .INDENT 3.5
@@ -1097,7 +1101,10 @@ Once the PR is successfully created, annotates the current branch with the new P
 .sp
 If \fB\&.git/info/milestone\fP file is present, its contents (a single number \-\-\- milestone id) are used as milestone.
 If \fB\&.git/info/reviewers\fP file is present, its contents (one GitHub login per line) are used to set reviewers.
+.sp
+The subject of the first unique commit of the branch is used as PR title.
 If \fB\&.git/info/description\fP or \fB\&.github/pull_request_template.md\fP file 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 the following header:
 .sp
@@ -1164,17 +1171,21 @@ deletes untracked managed branches that have no downstream branch.
 .UNINDENT
 .UNINDENT
 .sp
-\fBGit config keys (all subcommands):\fP
+\fBGit config keys:\fP
 .INDENT 0.0
 .TP
-.B \fBmachete.github.annotateWithUrls\fP:
+.B \fBmachete.github.annotateWithUrls\fP (all subcommands):
 Setting this config key to \fBtrue\fP will cause all commands that write GitHub PR numbers into annotations
 to not only include PR number and author (if different from the current user), but also the full URL of the PR.
 .sp
 The affected (sub)commands clearly include \fBanno \-\-sync\-github\-prs\fP and \fBgithub anno\-prs\fP,
 but also \fBgithub checkout\-prs\fP, \fBgithub create\-pr\fP, \fBgithub retarget\-pr\fP and \fBgithub restack\-pr\fP\&.
 .TP
-.B \fBmachete.github.{domain,remote,organization,repository}\fP:
+.B \fBmachete.github.forceDescriptionFromCommitMessage\fP (\fBcreate\-pr\fP only):
+Setting this config key to \fBtrue\fP will force \fBgit machete github create\-pr\fP to take PR description
+from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.github/pull_request_template.md\fP is present.
+.TP
+.B \fBmachete.github.{domain,remote,organization,repository}\fP (all subcommands):
 .INDENT 7.0
 .INDENT 3.5
 GitHub API server URL will be inferred from \fBgit remote\fP\&.
diff --git a/docs/source/cli/config.rst b/docs/source/cli/config.rst
index 073c4ce..0942369 100644
--- a/docs/source/cli/config.rst
+++ b/docs/source/cli/config.rst
@@ -11,6 +11,9 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
 ``machete.github.annotateWithUrls``:
   .. include:: github_annotateWithUrls_config_key.rst
 
+``machete.github.forceDescriptionFromCommitMessage``:
+  .. include:: github_forceDescriptionFromCommitMessage_config_key.rst
+
 ``machete.github.{domain,remote,organization,repository}``:
   .. include:: github_access_config_keys.rst
       :start-line: 3
diff --git a/docs/source/cli/github.rst b/docs/source/cli/github.rst
index 4b41815..0ccdc67 100644
--- a/docs/source/cli/github.rst
+++ b/docs/source/cli/github.rst
@@ -59,7 +59,10 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
 
     If ``.git/info/milestone`` file is present, its contents (a single number --- milestone id) are used as milestone.
     If ``.git/info/reviewers`` file is present, its contents (one GitHub login per line) are used to set reviewers.
+
+    The subject of the first unique commit of the branch is used as PR title.
     If ``.git/info/description`` or ``.github/pull_request_template.md`` file 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 the following header:
 
@@ -108,12 +111,15 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
       2. deletes unmanaged branches,
       3. deletes untracked managed branches that have no downstream branch.
 
-**Git config keys (all subcommands):**
+**Git config keys:**
 
-``machete.github.annotateWithUrls``:
+``machete.github.annotateWithUrls`` (all subcommands):
   .. include:: github_annotateWithUrls_config_key.rst
 
-``machete.github.{domain,remote,organization,repository}``:
+``machete.github.forceDescriptionFromCommitMessage`` (``create-pr`` only):
+  .. include:: github_forceDescriptionFromCommitMessage_config_key.rst
+
+``machete.github.{domain,remote,organization,repository}`` (all subcommands):
   .. include:: github_access_config_keys.rst
       :start-line: 2
 
diff --git a/docs/source/github_forceDescriptionFromCommitMessage_config_key.rst b/docs/source/github_forceDescriptionFromCommitMessage_config_key.rst
new file mode 100644
index 0000000..8d6232b
--- /dev/null
+++ b/docs/source/github_forceDescriptionFromCommitMessage_config_key.rst
@@ -0,0 +1,2 @@
+Setting this config key to ``true`` will force ``git machete github create-pr`` to take PR description
+from the message body of the first unique commit of the branch, even if ``.git/info/description`` and/or ``.github/pull_request_template.md`` is present.
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 9ad8de9..1ee469f 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.21.1'
+__version__ = '3.22.0'
diff --git a/git_machete/client.py b/git_machete/client.py
index f6b7182..7b19bca 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -16,6 +16,7 @@ from .constants import (DISCOVER_DEFAULT_FRESH_BRANCH_COUNT, PICK_FIRST_ROOT,
 from .exceptions import (InteractionStopped, MacheteException,
                          UnexpectedMacheteException,
                          UnprocessableEntityHTTPError)
+from .git_config_keys import GITHUB_FORCE_DESCRIPTION_FROM_COMMIT_MESSAGE
 from .git_operations import (HEAD, AnyBranchName, AnyRevision, BranchPair,
                              ForkPointOverrideData, FullCommitHash, GitContext,
                              GitLogEntry, LocalBranchShortName,
@@ -281,7 +282,8 @@ class MacheteClient:
                                 # Let's skip interactive questions so as not to confuse new users.
                                 self.__roots = [current_branch]
                                 self.__managed_branches = [current_branch]
-                                if verbose:
+                                # This section of code is only ever executed in verbose mode, but let's leave the `if` for consistency
+                                if verbose:  # pragma: no branch
                                     print(fmt(f"Added branch {bold(current_branch)} as a new root"))
                                 opt_onto = current_branch
                     self.__git.create_branch(branch, out_of, switch_head=switch_head_if_new_branch)
@@ -1520,8 +1522,7 @@ class MacheteClient:
             return
 
         earliest_commit = commits[0]
-        earliest_full_body = self.__git.get_commit_data(FullCommitHash.of(earliest_commit.hash), GitFormatPatterns.RAW_BODY).strip()
-        # %ai for ISO-8601 format; %aE/%aN for respecting .mailmap; see `git rev-list --help`
+        earliest_full_message = self.__git.get_commit_data(FullCommitHash.of(earliest_commit.hash), GitFormatPatterns.FULL_MESSAGE).strip()
         earliest_author_date = self.__git.get_commit_data(FullCommitHash.of(earliest_commit.hash),
                                                           GitFormatPatterns.AUTHOR_DATE).strip()
         earliest_author_email = self.__git.get_commit_data(FullCommitHash.of(earliest_commit.hash),
@@ -1541,7 +1542,7 @@ class MacheteClient:
         # otherwise the entire `commit-tree` will fail on some ancient supported
         # versions of git (at least on v1.7.10).
         squashed_hash = FullCommitHash.of(self.__git.commit_tree_with_given_parent_and_message_and_env(
-            opt_fork_point, earliest_full_body, author_env).strip())
+            opt_fork_point, earliest_full_message, author_env).strip())
 
         # This can't be done with `git reset` since it doesn't allow for a custom reflog message.
         # Even worse, reset's reflog message would be filtered out in our fork point algorithm,
@@ -1611,7 +1612,7 @@ class MacheteClient:
         anno = f"PR #{pr.number}"
         if current_user != pr.user:
             anno += f" ({pr.user})"
-        if include_url or self.__git.get_boolean_config_attr_or_none(key=git_config_keys.GITHUB_ANNOTATE_WITH_URLS):
+        if include_url or self.__git.get_boolean_config_attr(key=git_config_keys.GITHUB_ANNOTATE_WITH_URLS, default_value=False):
             anno += f" {pr.html_url}"
         return anno
 
@@ -2478,21 +2479,9 @@ class MacheteClient:
         debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}')
         debug('current GitHub user is ' + (current_user or '<none>'))
 
-        machete_description_path = self.__git.get_main_git_subpath('info', 'description')
-        # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository
-        github_template_path = os.path.join(self.__git.get_root_dir(), '.github', 'pull_request_template.md')
-        if os.path.isfile(machete_description_path):
-            description = utils.slurp_file(machete_description_path)
-        elif os.path.isfile(github_template_path):
-            description = utils.slurp_file(github_template_path)
-        else:
-            description = ''
-
-        if pr_number_for_base_branch:
-            description = f'# Based on PR #{pr_number_for_base_branch}\n\n' + description
-
         fork_point = self.fork_point(head, use_overrides=True)
         commits: List[GitLogEntry] = self.__git.get_commits_between(fork_point, head)
+
         if opt_title:
             title = opt_title
         else:
@@ -2501,6 +2490,24 @@ class MacheteClient:
             # Let's use branch name as a fallback for PR title in such case.
             title = commits[0].subject if commits else head
 
+        force_description_from_commit_message = self.__git.get_boolean_config_attr(
+            key=GITHUB_FORCE_DESCRIPTION_FROM_COMMIT_MESSAGE, default_value=False)
+        if force_description_from_commit_message:
+            description = self.__git.get_commit_data(commits[0].hash, GitFormatPatterns.MESSAGE_BODY) if commits else ''
+        else:
+            machete_description_path = self.__git.get_main_git_subpath('info', 'description')
+            # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository
+            github_template_path = os.path.join(self.__git.get_root_dir(), '.github', 'pull_request_template.md')
+            if os.path.isfile(machete_description_path):
+                description = utils.slurp_file(machete_description_path)
+            elif os.path.isfile(github_template_path):
+                description = utils.slurp_file(github_template_path)
+            else:
+                description = self.__git.get_commit_data(commits[0].hash, GitFormatPatterns.MESSAGE_BODY) if commits else ''
+
+        if pr_number_for_base_branch:
+            description = f'# Based on PR #{pr_number_for_base_branch}\n\n' + description
+
         ok_str = '<green><b>OK</b></green>'
         print(f'Creating a {"draft " if opt_draft else ""}PR from {bold(head)} to {bold(base)}... ', end='', flush=True)
 
diff --git a/git_machete/constants.py b/git_machete/constants.py
index 6064987..5e20ec2 100644
--- a/git_machete/constants.py
+++ b/git_machete/constants.py
@@ -18,7 +18,12 @@ class SyncToRemoteStatuses(IntEnum):
 
 
 class GitFormatPatterns(Enum):
-    AUTHOR_NAME = "%aN"
-    AUTHOR_EMAIL = "%aE"
+    # %ai for ISO-8601 format
     AUTHOR_DATE = "%ai"
-    RAW_BODY = "%B"
+    # %aE/%aN (rather than %ae/%an) for respecting .mailmap; see `git rev-list --help`
+    AUTHOR_EMAIL = "%aE"
+    AUTHOR_NAME = "%aN"
+    # subject and body
+    FULL_MESSAGE = "%B"
+    # subject NOT included
+    MESSAGE_BODY = "%b"
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index b25a53d..2049126 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -284,6 +284,10 @@ long_docs: Dict[str, str] = {
               The affected (sub)commands clearly include `anno --sync-github-prs` and `github anno-prs`,
               but also `github checkout-prs`, `github create-pr`, `github retarget-pr` and `github restack-pr`.
 
+           `machete.github.forceDescriptionFromCommitMessage`:
+              Setting this config key to `true` will force `git machete github create-pr` to take PR description
+              from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.github/pull_request_template.md` is present.
+
            `machete.github.{domain,remote,organization,repository}`:
 
               You can alter the default behavior by setting the following git config keys:
@@ -619,7 +623,10 @@ long_docs: Dict[str, str] = {
 
               If `.git/info/milestone` file is present, its contents (a single number — milestone id) are used as milestone.
               If `.git/info/reviewers` file is present, its contents (one GitHub login per line) are used to set reviewers.
+
+              The subject of the first unique commit of the branch is used as PR title.
               If `.git/info/description` or `.github/pull_request_template.md` file 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 the following header:
 
@@ -681,8 +688,8 @@ long_docs: Dict[str, str] = {
 
                  * deletes untracked managed branches that have no downstream branch.
 
-        <b>Git config keys (all subcommands):</b>
-           `machete.github.annotateWithUrls`:
+        <b>Git config keys:</b>
+           `machete.github.annotateWithUrls` (all subcommands):
  
               Setting this config key to `true` will cause all commands that write GitHub PR numbers into annotations
               to not only include PR number and author (if different from the current user), but also the full URL of the PR.
@@ -690,7 +697,11 @@ long_docs: Dict[str, str] = {
               The affected (sub)commands clearly include `anno --sync-github-prs` and `github anno-prs`,
               but also `github checkout-prs`, `github create-pr`, `github retarget-pr` and `github restack-pr`.
 
-           `machete.github.{domain,remote,organization,repository}`:
+           `machete.github.forceDescriptionFromCommitMessage` (`create-pr` only):
+              Setting this config key to `true` will force `git machete github create-pr` to take PR description
+              from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.github/pull_request_template.md` is present.
+
+           `machete.github.{domain,remote,organization,repository}` (all subcommands):
 
               GitHub API server URL will be inferred from `git remote`.
               You can alter the default behavior by setting the following git config keys:
diff --git a/git_machete/git_config_keys.py b/git_machete/git_config_keys.py
index a214b31..4be63db 100644
--- a/git_machete/git_config_keys.py
+++ b/git_machete/git_config_keys.py
@@ -1,11 +1,12 @@
 GITHUB_ANNOTATE_WITH_URLS = 'machete.github.annotateWithUrls'
+GITHUB_FORCE_DESCRIPTION_FROM_COMMIT_MESSAGE = 'machete.github.forceDescriptionFromCommitMessage'
 GITHUB_DOMAIN = 'machete.github.domain'
 GITHUB_REMOTE = 'machete.github.remote'
 GITHUB_ORGANIZATION = 'machete.github.organization'
 GITHUB_REPOSITORY = 'machete.github.repository'
+STATUS_EXTRA_SPACE_BEFORE_BRANCH_NAME = 'machete.status.extraSpaceBeforeBranchName'
 TRAVERSE_PUSH = 'machete.traverse.push'
 WORKTREE_USE_TOP_LEVEL_MACHETE_FILE = 'machete.worktree.useTopLevelMacheteFile'
-STATUS_EXTRA_SPACE_BEFORE_BRANCH_NAME = 'machete.status.extraSpaceBeforeBranchName'
 
 
 def override_fork_point_to(branch: str) -> str:
diff --git a/requirements/typos.txt b/requirements/typos.txt
new file mode 100644
index 0000000..a6137fe
--- /dev/null
+++ b/requirements/typos.txt
@@ -0,0 +1,1 @@
+typos==1.16.23
diff --git a/tox.ini b/tox.ini
index 1922058..5b7faf0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -177,3 +177,9 @@ deps =
   -r{[requirements]dir}/mypy.txt
 commands =
   mypy --config-file mypy.ini git_machete tests
+
+[testenv:typos-check]
+deps =
+  -r{[requirements]dir}/typos.txt
+commands =
+  typos --ignore --format brief


Test patch:
diff --git a/tests/base_test.py b/tests/base_test.py
index b60c780..f050326 100644
--- a/tests/base_test.py
+++ b/tests/base_test.py
@@ -87,7 +87,9 @@ class GitRepositorySandbox:
         self.file_counter += 1
         self.execute(f"touch {f}")
         self.execute(f"git add {f}")
-        self.execute(f'git commit -m "{message}"')
+        self.write_to_file(".git/commit-message", message)
+        # Not passing the message directly via `-m` so that multiline messages can be handled correctly on Windows.
+        self.execute('git commit --file=.git/commit-message')
         return self
 
     def commit_n_times(self, count: int) -> "GitRepositorySandbox":
diff --git a/tests/test_add.py b/tests/test_add.py
index b97b98f..8eb5e4e 100644
--- a/tests/test_add.py
+++ b/tests/test_add.py
@@ -182,6 +182,38 @@ class TestAdd(BaseTest):
             """)
         assert read_branch_layout_file() == "master\n  foo\n"
 
+    def test_add_new_branch_with_onto(self) -> None:
+        self.repo_sandbox\
+            .new_branch("master").commit()\
+            .new_branch("develop").commit()
+
+        body: str = \
+            """
+            master
+              develop
+            """
+        rewrite_branch_layout_file(body)
+
+        assert_success(
+            ['add', '--onto=master', '--yes', 'foo'],
+            """
+            A local branch foo does not exist. Creating out of master
+            Added branch foo onto master
+            """)
+        assert read_branch_layout_file() == "master\n  develop\n  foo\n"
+        assert self.repo_sandbox.get_commit_hash("master") == self.repo_sandbox.get_commit_hash("foo")
+
+    def test_add_new_branch_when_detached_head_for_fresh_start(self) -> None:
+        self.repo_sandbox.new_branch("master").commit("master commit.")\
+            .check_out(self.repo_sandbox.get_current_commit_hash())
+        assert_success(
+            ['add', '--yes', 'foo'],
+            """
+            A local branch foo does not exist. Creating out of the current HEAD
+            Added branch foo as a new root
+            """)
+        assert read_branch_layout_file() == "foo\n"
+
     def test_add_new_branch_onto_master_for_fresh_start_without_yes(self, mocker: MockerFixture) -> None:
         self.repo_sandbox.new_branch("master").commit("master commit.")
 
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index fdd8eb2..10cba02 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -110,6 +110,7 @@ class TestGitHubCreatePR(BaseTest):
               x-drop-constraint (untracked)
             """,
         )
+
         # untracked state (can only create PR when branch is pushed)
         self.repo_sandbox.check_out('chore/fields')
 
@@ -300,8 +301,8 @@ class TestGitHubCreatePR(BaseTest):
     def test_github_create_pr_missing_base_branch_on_remote(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none)
-        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(
-            self.github_api_state_for_test_create_pr_missing_base_branch_on_remote()))
+        github_api_state = self.github_api_state_for_test_create_pr_missing_base_branch_on_remote()
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(github_api_state))
 
         (
             self.repo_sandbox.new_branch("root")
@@ -312,7 +313,7 @@ class TestGitHubCreatePR(BaseTest):
                 .new_branch("feature/api_handling")
                 .commit("Introduce GET and POST methods on API")
                 .new_branch("feature/api_exception_handling")
-                .commit("catch exceptions coming from API")
+                .commit("catch exceptions coming from API\n\ncommit body\nanother line")
                 .push()
                 .delete_branch("root")
         )
@@ -341,6 +342,9 @@ class TestGitHubCreatePR(BaseTest):
               o-feature/api_exception_handling *  PR #19 (some_other_user) www.github.com
             """,
         )
+        pr = github_api_state.get_pull_by_number(19)
+        assert pr is not None
+        assert pr['body'] == 'commit body\nanother line'
 
     @staticmethod
     def github_api_state_for_test_github_create_pr_with_multiple_non_origin_remotes() -> MockGitHubAPIState:
@@ -351,8 +355,8 @@ class TestGitHubCreatePR(BaseTest):
     def test_github_create_pr_with_multiple_non_origin_remotes(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none)
-        self.patch_symbol(mocker, 'urllib.request.urlopen',
-                          mock_urlopen(self.github_api_state_for_test_github_create_pr_with_multiple_non_origin_remotes()))
+        github_api_state = self.github_api_state_for_test_github_create_pr_with_multiple_non_origin_remotes()
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(github_api_state))
 
         origin_1_remote_path = mkdtemp()
         origin_2_remote_path = mkdtemp()
@@ -374,7 +378,7 @@ class TestGitHubCreatePR(BaseTest):
                 .push(remote='origin_1')
                 .push(remote='origin_2')
                 .new_branch('feature')
-                .commit('introduce feature')
+                .commit('introduce feature\n\ncommit body')
                 .push(remote='origin_1', set_upstream=False)
                 .push(remote='origin_2', set_upstream=False)
         )
@@ -460,10 +464,15 @@ class TestGitHubCreatePR(BaseTest):
         Creating a PR from feature to branch-1... OK, see www.github.com
         """  # noqa: E501
 
+        self.repo_sandbox.write_to_file(".git/info/description", "overridden description")
+        self.repo_sandbox.set_git_config_key("machete.github.forceDescriptionFromCommitMessage", "true")
         assert_success(
             ['github', 'create-pr'],
             expected_result
         )
+        pr = github_api_state.get_pull_by_number(16)
+        assert pr is not None
+        assert pr['body'] == '# Based on PR #15\n\ncommit body'
 
         # branch feature_1 present in each of the remotes, tracking data present
         (