Repository: VirtusLab/git-machete
Pull request #1060 (2023-09-24 14:02:01)

Add comment linking the base PR via `github create-pr`
# Based on PR #1070


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1060?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) Report
Patch coverage: **`89.47%`** and project coverage change: **`-0.06%`** :warning:
> Comparison is base [(`d369c74`)](https://app.codecov.io/gh/VirtusLab/git-machete/commit/d369c74b440cd3aa5eef0dfbda69b656ce6f31d9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) 98.73% compared to head [(`4da721a`)](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1060?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) 98.68%.

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


```diff
@@             Coverage Diff             @@
##           develop    #1060      +/-   ##
===========================================
- Coverage    98.73%   98.68%   -0.06%     
===========================================
  Files           12       12              
  Lines         3173     3183      +10     
  Branches       714      717       +3     
===========================================
+ Hits          3133     3141       +8     
- Misses          25       26       +1     
- Partials        15       16       +1     
```


| [Files Changed](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1060?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) | Coverage Δ | |
|---|---|---|
| [git\_machete/generated\_docs.py](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1060?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab#diff-Z2l0X21hY2hldGUvZ2VuZXJhdGVkX2RvY3MucHk=) | `100.00% <ø> (ø)` | |
| [git\_machete/client.py](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1060?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab#diff-Z2l0X21hY2hldGUvY2xpZW50LnB5) | `99.39% <87.50%> (-0.14%)` | :arrow_down: |
| [git\_machete/github.py](https://app.codecov.io/gh/VirtusLab/git-machete/pull/1060?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab#diff-Z2l0X21hY2hldGUvZ2l0aHViLnB5) | `98.33% <100.00%> (ø)` | |


</details>

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


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index b506e6d..ea6f1ce 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,6 +2,8 @@
 
 ## New in git-machete 3.20.0
 
+- added: `git machete github create-pr` adds a comment linking the PR to its base PR; `github retarget-pr` keeps that comment up to date (suggested by @guyboltonking)
+
 ## New in git-machete 3.19.1
 
 - improved: when running `git machete squash` against a root branch, the error message suggests using `--fork-point=...` flag (suggested by @levinotik)
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index f9f0286..a986e1c 100644
--- a/docs/man/git-machete.1
+++ b/docs/man/git-machete.1
@@ -1106,9 +1106,13 @@ Checkout open PRs for the current user associated with the GitHub token.
 Creates a PR for the current branch, using the upstream (parent) branch as the PR base.
 Once the PR is successfully created, annotates the current branch with the new PR\(aqs number.
 .sp
-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/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.
+If \fB\&.git/info/description\fP or \fB\&.github/pull_request_template.md\fP file is present, its contents are used as PR description.
+.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
+\fB# Based on PR #<number of PR for base branch>\fP
 .sp
 \fBOptions:\fP
 .INDENT 7.0
@@ -1120,6 +1124,12 @@ Creates the new PR as a draft.
 .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 at GitHub will be prepended with the following header:
+.sp
+\fB# Based on PR #<number of PR for base branch>\fP
+.sp
+This header will be updated or removed accordingly with the subsequent runs of \fBretarget\-pr\fP\&.
+.sp
 \fBOptions:\fP
 .INDENT 7.0
 .TP
diff --git a/docs/source/cli/github.rst b/docs/source/cli/github.rst
index 5d7f0eb..baa716e 100644
--- a/docs/source/cli/github.rst
+++ b/docs/source/cli/github.rst
@@ -49,9 +49,13 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
     Creates a PR for the current branch, using the upstream (parent) branch as the PR base.
     Once the PR is successfully created, annotates the current branch with the new PR's number.
 
-    If ``.git/info/description`` or ``.github/pull_request_template.md`` file is present, its contents are used as PR description.
     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.
+    If ``.git/info/description`` or ``.github/pull_request_template.md`` file is present, its contents are used as PR description.
+
+    If the newly-created PR is stacked atop another PR, the actual PR description posted to GitHub will be prepended with the following header:
+
+    ``# Based on PR #<number of PR for base branch>``
 
     **Options:**
 
@@ -60,6 +64,12 @@ 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 at GitHub will be prepended with the following header:
+
+    ``# Based on PR #<number of PR for base branch>``
+
+    This header will be updated or removed accordingly with the subsequent runs of ``retarget-pr``.
+
     **Options:**
 
     -b, --branch=<branch>     Specify the branch for which the associated PR base will be set to its upstream (parent) branch. The current branch is used if the option is absent.
diff --git a/git_machete/client.py b/git_machete/client.py
index 1fa8f1a..300287f 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -1591,9 +1591,9 @@ class MacheteClient:
         org_repo_remote = self.__derive_org_repo_and_remote(domain=domain)
         github_client = GitHubClient(domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
         print('Checking for open GitHub PRs... ', end='', flush=True)
-        current_user: Optional[str] = github_client.derive_current_user_login()
+        current_user: Optional[str] = github_client.get_current_user_login()
         debug('Current GitHub user is ' + (bold(current_user or '<none>')))
-        all_open_prs: List[GitHubPullRequest] = github_client.derive_open_pull_requests()
+        all_open_prs: List[GitHubPullRequest] = github_client.get_open_pull_requests()
         print(fmt('<green><b>OK</b></green>'))
         self.__sync_annotations_to_branch_layout_file(all_open_prs, current_user, verbose=True)
 
@@ -2034,7 +2034,7 @@ class MacheteClient:
         github_client = GitHubClient(domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
         print('Checking for open GitHub PRs... ', end='', flush=True)
 
-        current_user: Optional[str] = github_client.derive_current_user_login()
+        current_user: Optional[str] = github_client.get_current_user_login()
         if not current_user and mine:
             msg = ("Could not determine current user name, please check that the GitHub API token provided by one of the: "
                    f"{GitHubToken.get_possible_providers()}is valid.")
@@ -2043,7 +2043,7 @@ class MacheteClient:
             else:
                 warn(msg)
                 return
-        all_open_prs: List[GitHubPullRequest] = github_client.derive_open_pull_requests()
+        all_open_prs: List[GitHubPullRequest] = github_client.get_open_pull_requests()
         print(fmt('<green><b>OK</b></green>'))
 
         applicable_prs: List[GitHubPullRequest] = self.__get_applicable_pull_requests(
@@ -2179,6 +2179,19 @@ class MacheteClient:
                 return remote
         return None
 
+    @staticmethod
+    def __get_updated_github_pr_description(old_description: Optional[str], pr_number_for_base_branch: Optional[int]) -> str:
+        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 []
+        if lines and '# Based on PR #' in lines[0]:
+            lines = lines[1:]
+        lines = skip_leading_empty(lines)
+        if pr_number_for_base_branch:
+            lines = [f'# Based on PR #{pr_number_for_base_branch}', ''] + lines
+        return '\n'.join(lines)
+
     def retarget_github_pr(self, head: LocalBranchShortName, ignore_if_missing: bool) -> None:
         domain = self.__derive_github_domain()
         org_repo_remote = self.__derive_org_repo_and_remote(domain=domain, branch_used_for_tracking_data=head)
@@ -2186,7 +2199,7 @@ class MacheteClient:
 
         debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}')
 
-        prs: List[GitHubPullRequest] = github_client.derive_open_pull_requests_by_head(head)
+        prs: List[GitHubPullRequest] = github_client.get_open_pull_requests_by_head(head)
         if not prs:
             if ignore_if_missing:
                 warn(f"no PRs have `{head}` as its head")
@@ -2208,9 +2221,22 @@ class MacheteClient:
 
         if pr.base != new_base:
             github_client.set_base_of_pull_request(pr.number, base=new_base)
-            print(f'The base branch of PR #{bold(str(pr.number))} has been switched to {bold(new_base)}')
+            print(f'Base branch of PR #{bold(str(pr.number))} has been switched to {bold(new_base)}')
         else:
-            print(f'The base branch of PR #{bold(str(pr.number))} is already {bold(new_base)}')
+            print(f'Base branch of PR #{bold(str(pr.number))} is already {bold(new_base)}')
+
+        pr_number_for_base_branch: Optional[int] = None
+        prs_for_base_branch = github_client.get_open_pull_requests_by_head(new_base)
+        if len(prs_for_base_branch) >= 1:
+            pr_number_for_base_branch = prs_for_base_branch[0].number
+        new_description = self.__get_updated_github_pr_description(pr.description, pr_number_for_base_branch)
+        if pr.description != new_description:
+            github_client.set_description_of_pull_request(pr.number, description=new_description)
+            if not pr_number_for_base_branch:
+                print(f'Base PR header has been removed from the description of PR #{bold(str(pr.number))}')
+            else:
+                print(f'Base PR header in the description of PR #{bold(str(pr.number))} '
+                      f'now points to PR #{bold(str(pr_number_for_base_branch))}')
 
         if self.__annotations.get(head) and self.__annotations[head].qualifiers_text:
             self.__annotations[head] = Annotation(f'PR #{pr.number} ' + self.__annotations[head].qualifiers_text)
@@ -2324,6 +2350,7 @@ class MacheteClient:
         github_client = GitHubClient(domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository)
         print(f"Fetching {bold(org_repo_remote.remote)}...")
         self.__git.fetch_remote(org_repo_remote.remote)
+        pr_number_for_base_branch: Optional[int] = None
         if '/'.join([org_repo_remote.remote, base]) not in self.__git.get_remote_branches():
             warn(f'Base branch for this PR ({bold(base)}) is not found on remote, pushing...')
             self.__handle_untracked_branch(
@@ -2334,8 +2361,12 @@ class MacheteClient:
                 opt_push_tracked=False,
                 opt_push_untracked=True,
                 opt_yes=False)
+        else:
+            prs_for_base_branch = github_client.get_open_pull_requests_by_head(base)
+            if len(prs_for_base_branch) >= 1:
+                pr_number_for_base_branch = prs_for_base_branch[0].number
 
-        current_user: Optional[str] = github_client.derive_current_user_login()
+        current_user: Optional[str] = github_client.get_current_user_login()
         debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}')
         debug('current GitHub user is ' + (current_user or '<none>'))
 
@@ -2349,6 +2380,9 @@ class MacheteClient:
         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)
         # git-machete can still see an empty range of unique commits (e.g. in case of yellow edge)
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index b3586eb..bd8df88 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -615,9 +615,13 @@ long_docs: Dict[str, str] = {
               Creates a PR for the current branch, using the upstream (parent) branch as the PR base.
               Once the PR is successfully created, annotates the current branch with the new PR's number.
 
-              If `.git/info/description` or `.github/pull_request_template.md` file is present, its contents are used as PR description.
               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.
+              If `.git/info/description` or `.github/pull_request_template.md` file is present, its contents are used as PR description.
+
+              If the newly-created PR is stacked atop another PR, the actual PR description posted to GitHub will be prepended with the following header:
+
+              `# Based on PR #<number of PR for base branch>`
 
               <b>Options:</b>
 
@@ -628,6 +632,12 @@ 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 at GitHub will be prepended with the following header:
+
+              `# Based on PR #<number of PR for base branch>`
+
+              This header will be updated or removed accordingly with the subsequent runs of `retarget-pr`.
+
               <b>Options:</b>
 
                  <b>-b</b>, <b>--branch=<branch></b>
diff --git a/git_machete/github.py b/git_machete/github.py
index c87fc75..75abb2f 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -358,6 +358,12 @@ class GitHubClient:
                                        path=f'/repos/{self.__organization}/{self.__repository}/pulls/{number}',
                                        request_body=request_body)
 
+    def set_description_of_pull_request(self, number: int, description: str) -> None:
+        request_body: Dict[str, str] = {'body': description}
+        self.__fire_github_api_request(method='PATCH',
+                                       path=f'/repos/{self.__organization}/{self.__repository}/pulls/{number}',
+                                       request_body=request_body)
+
     def set_milestone_of_pull_request(self, number: int, milestone: str) -> None:
         request_body: Dict[str, str] = {'milestone': milestone}
         # Setting milestone is only available via the Issues API, not PRs API.
@@ -365,17 +371,17 @@ class GitHubClient:
                                        path=f'/repos/{self.__organization}/{self.__repository}/issues/{number}',
                                        request_body=request_body)
 
-    def derive_open_pull_requests_by_head(self, head: LocalBranchShortName) -> List[GitHubPullRequest]:
+    def get_open_pull_requests_by_head(self, head: LocalBranchShortName) -> List[GitHubPullRequest]:
         path = f'/repos/{self.__organization}/{self.__repository}/pulls?head={self.__organization}:{head}'
         prs = self.__fire_github_api_request(method='GET', path=path)
         return [GitHubPullRequest.from_json(pr) for pr in prs]
 
-    def derive_open_pull_requests(self) -> List[GitHubPullRequest]:
+    def get_open_pull_requests(self) -> List[GitHubPullRequest]:
         path = f'/repos/{self.__organization}/{self.__repository}/pulls?per_page={self.MAX_PULLS_PER_PAGE_COUNT}'
         prs = self.__fire_github_api_request(method='GET', path=path)
         return list(map(GitHubPullRequest.from_json, prs))
 
-    def derive_current_user_login(self) -> Optional[str]:
+    def get_current_user_login(self) -> Optional[str]:
         if not self.__token:
             return None
         user = self.__fire_github_api_request(method='GET', path='/user')


Test patch:
diff --git a/tests/mockers_github.py b/tests/mockers_github.py
index be11ceb..89a97f4 100644
--- a/tests/mockers_github.py
+++ b/tests/mockers_github.py
@@ -50,9 +50,9 @@ class MockGitHubAPIState:
     def __init__(self, pulls: List[Dict[str, Any]]) -> None:
         self.__pulls: List[Dict[str, Any]] = [dict(pull) for pull in pulls]
 
-    def get_pull_by_number(self, pull_no: str) -> Optional[Dict[str, Any]]:
+    def get_pull_by_number(self, pull_no: int) -> Optional[Dict[str, Any]]:
         for pull in self.__pulls:
-            if pull['number'] == pull_no:
+            if pull['number'] == str(pull_no):
                 return pull
         return None
 
@@ -142,10 +142,10 @@ def __mock_urlopen_impl(github_api_state: MockGitHubAPIState, request: Request)
                     link_header = ''
                 return MockGitHubAPIResponse(HTTPStatus.OK, response_data=pulls[start:end], headers={'link': link_header})
         elif url_path_matches('/repos/*/*/pulls/[0-9]+'):
-            number = url_segments[-1]
-            prs_ = github_api_state.get_pull_by_number(number)
-            if prs_:
-                return MockGitHubAPIResponse(HTTPStatus.OK, prs_)
+            pull_no = int(url_segments[-1])
+            pull = github_api_state.get_pull_by_number(pull_no)
+            if pull:
+                return MockGitHubAPIResponse(HTTPStatus.OK, pull)
             raise error_404()
         elif url_path_matches('/user'):
             return MockGitHubAPIResponse(HTTPStatus.OK, {'login': 'github_user', 'type': 'User', 'company': 'VirtusLab'})
@@ -170,8 +170,8 @@ def __mock_urlopen_impl(github_api_state: MockGitHubAPIState, request: Request)
                 raise error_422({'message': 'Validation Failed', 'errors': [
                     {'message': f'A pull request already exists for test_repo:{head}.'}]})
             return create_pull_request()
-        elif url_path_matches("/repos/*/*/(pulls|issues)/[0-9]+/*"):
-            pull_no = url_segments[-2]  # e.g. /repos/example-org/example-repo/pulls/5/requested_reviewers
+        elif url_path_matches("/repos/*/*/(pulls|issues)/[0-9]+/(assignees|requested_reviewers)"):
+            pull_no = int(url_segments[-2])
             pull = github_api_state.get_pull_by_number(pull_no)
             assert pull is not None
             if "invalid-user" in list(json_data.values())[0]:
@@ -186,7 +186,7 @@ def __mock_urlopen_impl(github_api_state: MockGitHubAPIState, request: Request)
             raise error_404()
 
     def update_pull_request() -> "MockGitHubAPIResponse":
-        pull_no = url_segments[-1]
+        pull_no = int(url_segments[-1])
         pull = github_api_state.get_pull_by_number(pull_no)
         assert pull is not None
         fill_pull_request_from_json_data(pull)
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index 6a34e7a..c08c1db 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -83,7 +83,7 @@ class TestGitHubCreatePR(BaseTest):
             """
             master
                 hotfix/add-trigger
-                    ignore-trailing
+                    ignore-trailing  PR #3
                         chore/fields
             develop
                 allow-ownership-link
@@ -102,7 +102,7 @@ class TestGitHubCreatePR(BaseTest):
             |
             o-hotfix/add-trigger (diverged from origin)
               |
-              o-ignore-trailing (diverged from & older than origin)
+              o-ignore-trailing  PR #3 (diverged from & older than origin)
                 |
                 o-chore/fields (untracked)
 
@@ -132,7 +132,7 @@ class TestGitHubCreatePR(BaseTest):
               |
               o-hotfix/add-trigger (diverged from origin)
                 |
-                o-ignore-trailing (diverged from & older than origin)
+                o-ignore-trailing  PR #3 (diverged from & older than origin)
                   |
                   o-chore/fields *
 
@@ -153,6 +153,14 @@ class TestGitHubCreatePR(BaseTest):
             Adding foo, bar as reviewers to PR #5... OK
             """
         )
+        pr = self.github_api_state_for_test_create_pr.get_pull_by_number(5)
+        assert pr is not None
+        assert pr['body'] == '# Based on PR #3\n\n# PR title\n## Summary\n##Test plan\n'
+        assert pr['draft'] is True
+        assert pr['milestone'] == '42'
+        assert pr['assignees'] == ['github_user']
+        assert pr['reviewers'] == ['foo', 'bar']
+
         assert_success(
             ['status'],
             """
@@ -160,7 +168,7 @@ class TestGitHubCreatePR(BaseTest):
             |
             o-hotfix/add-trigger (diverged from origin)
               |
-              o-ignore-trailing (diverged from & older than origin)
+              o-ignore-trailing  PR #3 (diverged from & older than origin)
                 |
                 o-chore/fields *  PR #5
 
@@ -191,7 +199,7 @@ class TestGitHubCreatePR(BaseTest):
             |
             o-hotfix/add-trigger *  PR #6
               |
-              x-ignore-trailing (diverged from & older than origin)
+              x-ignore-trailing  PR #3 (diverged from & older than origin)
                 |
                 o-chore/fields  PR #5
 
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index 63631d2..557af80 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -25,14 +25,14 @@ class TestGitHubRetargetPR(BaseTest):
                 'user': {'login': 'some_other_user'},
                 'base': {'ref': 'master'}, 'number': '20',
                 'html_url': 'www.github.com',
-                'body': '# Summary', 'state': 'open'
+                'body': '# Summary\n\n', 'state': 'open'
             },
             {
                 'head': {'ref': 'feature_2', 'repo': mock_repository_info},
                 'user': {'login': 'some_other_user'},
                 'base': {'ref': 'master'}, 'number': '25',
                 'html_url': 'www.github.com',
-                'body': '# Summary', 'state': 'open'
+                'body': None, 'state': 'open'
             },
             {
                 'head': {'ref': 'feature_3', 'repo': mock_repository_info},
@@ -111,7 +111,7 @@ class TestGitHubRetargetPR(BaseTest):
             It looks like the organization or repository name got changed recently and is outdated.
             New organization is example-org and new repository is example-repo.
             You can update your remote repository via: git remote set-url <remote_name> <new_repository_url>.
-            The base branch of PR #15 has been switched to develop
+            Base branch of PR #15 has been switched to develop
             """
         )
 
@@ -131,7 +131,7 @@ class TestGitHubRetargetPR(BaseTest):
 
         assert_success(
             ['github', 'retarget-pr'],
-            'The base branch of PR #15 is already develop\n'
+            'Base branch of PR #15 is already develop\n'
         )
 
         self.repo_sandbox.check_out("feature_4")
@@ -203,7 +203,7 @@ class TestGitHubRetargetPR(BaseTest):
 
         assert_success(
             ['github', 'retarget-pr', '--branch', 'feature'],
-            'The base branch of PR #15 has been switched to branch-1\n'
+            'Base branch of PR #15 has been switched to branch-1\n'
         )
 
         expected_status_output = """
@@ -295,8 +295,13 @@ class TestGitHubRetargetPR(BaseTest):
 
         assert_success(
             ['github', 'retarget-pr'],
-            'The base branch of PR #20 has been switched to feature\n'
+            'Base branch of PR #20 has been switched to feature\n'
+            'Base PR header in the description of PR #20 now points to PR #15\n'
         )
+        pr20 = self.github_api_state_for_test_retarget_pr.get_pull_by_number(20)
+        assert pr20 is not None
+        assert pr20['base']['ref'] == 'feature'
+        assert pr20['body'] == '# Based on PR #15\n\n# Summary\n\n'
 
         # branch feature_2 is not present in any of the remotes
         (
@@ -325,8 +330,13 @@ class TestGitHubRetargetPR(BaseTest):
 
         assert_success(
             ['github', 'retarget-pr'],
-            'The base branch of PR #25 has been switched to feature\n'
+            'Base branch of PR #25 has been switched to feature\n'
+            'Base PR header in the description of PR #25 now points to PR #15\n'
         )
+        pr25 = self.github_api_state_for_test_retarget_pr.get_pull_by_number(25)
+        assert pr25 is not None
+        assert pr25['base']['ref'] == 'feature'
+        assert pr25['body'] == '# Based on PR #15\n'
 
         # branch feature_3 present in only one remote: origin_1 and has tracking data
         (
@@ -349,8 +359,55 @@ class TestGitHubRetargetPR(BaseTest):
 
         assert_success(
             ['github', 'retarget-pr'],
-            'The base branch of PR #30 has been switched to feature_2\n'
+            'Base branch of PR #30 has been switched to feature_2\n'
+            'Base PR header in the description of PR #30 now points to PR #25\n'
+        )
+        pr30 = self.github_api_state_for_test_retarget_pr.get_pull_by_number(30)
+        assert pr30 is not None
+        assert pr30['base']['ref'] == 'feature_2'
+        assert pr30['body'] == '# Based on PR #25\n\n# Summary'
+
+        body = \
+            """
+            root
+                branch-1
+                    feature
+                        feature_1
+                        feature_2
+                        feature_3
+            """
+        rewrite_branch_layout_file(body)
+
+        assert_success(
+            ['github', 'retarget-pr'],
+            'Base branch of PR #30 has been switched to feature\n'
+            'Base PR header in the description of PR #30 now points to PR #15\n'
+        )
+        pr30 = self.github_api_state_for_test_retarget_pr.get_pull_by_number(30)
+        assert pr30 is not None
+        assert pr30['base']['ref'] == 'feature'
+        assert pr30['body'] == '# Based on PR #15\n\n# Summary'
+
+        body = \
+            """
+            root
+                branch-1
+                    feature
+                        feature_1
+                        feature_2
+                feature_3
+            """
+        rewrite_branch_layout_file(body)
+
+        assert_success(
+            ['github', 'retarget-pr'],
+            'Base branch of PR #30 has been switched to root\n'
+            'Base PR header has been removed from the description of PR #30\n'
         )
+        pr30 = self.github_api_state_for_test_retarget_pr.get_pull_by_number(30)
+        assert pr30 is not None
+        assert pr30['base']['ref'] == 'root'
+        assert pr30['body'] == '# Summary'
 
     github_api_state_for_test_retarget_pr_root_branch = MockGitHubAPIState([{
         'head': {'ref': 'master', 'repo': mock_repository_info},