Repository: VirtusLab/git-machete
Pull request #1171 (2024-02-08 11:43:57)

Release v3.23.1


Patch:
diff --git a/README.md b/README.md
index 8918278..0f71a43 100644
--- a/README.md
+++ b/README.md
@@ -179,7 +179,7 @@ git machete github create-pr [--draft]
 The entire chain of PRs will be posted in the PR description: <br/>
 
 [//]: # (The image is referenced by its full URL to ensure it renders correctly on https://pypi.org/project/git-machete/)
-<img src="https://raw.githubusercontent.com/VirtusLab/git-machete/develop/graphics/pr-chain-github-screenshot.png"
+<img src="https://raw.githubusercontent.com/VirtusLab/git-machete/master/graphics/pr-chain-github-screenshot.png"
      alt="PR chain on GitHub"
      width="75%" />
 
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 98953ee..e6be018 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,9 @@
 # Release notes
 
+## New in git-machete 3.23.1
+
+- fixed: if a PR has a pre-v3.23.0 `Based on PR #...` header, then it's removed by `git machete github retarget-pr` in favor of the new extended PR chain
+
 ## New in git-machete 3.23.0
 
 - added: full chain of PRs (and not just a link to the base PR) is added to/updated in PR description by `git machete github create-pr`/`retarget-pr`/`restack-pr` (suggested by @mjgigli)
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 0d1e56d..a7ed6fc 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 04, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Feb 08, 2024" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.23.0
+git-machete \- git-machete 3.23.1
 .sp
 git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
 .sp
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index e3f9d8e..9571cb3 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.23.0'
+__version__ = '3.23.1'
diff --git a/git_machete/client.py b/git_machete/client.py
index 631d811..7dee476 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -2048,7 +2048,6 @@ class MacheteClient:
         domain = self.__derive_github_domain()
         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.get_current_user_login()
         if not current_user and mine:
@@ -2059,6 +2058,7 @@ class MacheteClient:
             else:
                 warn(msg)
                 return
+        print('Checking for open GitHub PRs... ', end='', flush=True)
         all_open_prs: List[GitHubPullRequest] = github_client.get_open_pull_requests()
         print(fmt('<green><b>OK</b></green>'))
 
@@ -2305,6 +2305,9 @@ class MacheteClient:
             else:
                 lines = lines[:start_index] + lines_to_prepend[:-1] + lines[end_index + 1:]
         else:
+            # For compatibility with pre-v3.23.0 format
+            if lines and '# Based on PR #' in lines[0]:
+                lines = skip_leading_empty(lines[1:])
             lines = lines_to_prepend + lines
         return '\n'.join(lines)
 
@@ -2452,7 +2455,9 @@ class MacheteClient:
         # We could just fetch them straight away... but this list can be quite long for commercial monorepos,
         # esp. given that GitHub limits the single page to 100 PRs (so multiple HTTP requests would be needed).
         # As a slight optimization, let's fetch the full PR list only if the current PR has a base PR at all.
+        print('Checking for open GitHub PRs (to determine PR chain)... ', end='', flush=True)
         all_open_prs: List[GitHubPullRequest] = github_client.get_open_pull_requests()
+        print(fmt('<green><b>OK</b></green>'))
         pr_path = self.__get_path_from_pr_chain(pr, all_open_prs)
 
         prepend = f'{self.START_GIT_MACHETE_GENERATED_COMMENT}\n\n'
@@ -2539,16 +2544,16 @@ class MacheteClient:
                                                                   description=description, draft=opt_draft)
         print(fmt(f'{ok_str}, see `{pr.html_url}`'))
 
-        # If base branch has not originally been found on the remote,
-        # we can be sure that a longer chain of PRs above the newly-created PR doesn't exist
+        # If base branch has NOT originally been found on the remote,
+        # we can be sure that a longer chain of PRs above the newly-created PR does NOT exist
         if base_branch_found_on_remote:
             # As the description may include the reference to this PR itself (in case of a chain of >=2 PRs),
             # let's update the PR description after it's already created (so that we know the current PR's number).
-            prepend = self.__generate_text_to_prepend_to_pr_description(github_client, pr)
-            if prepend:
+            text_to_prepend = self.__generate_text_to_prepend_to_pr_description(github_client, pr)
+            if text_to_prepend:
                 if description:
-                    prepend += '\n'
-                description = prepend + description
+                    text_to_prepend += '\n'
+                description = text_to_prepend + description
                 print(f'Updating description of PR #{bold(str(pr.number))} to include the chain of PRs... ', end='', flush=True)
                 github_client.set_description_of_pull_request(pr.number, description)
                 print(fmt(ok_str))
diff --git a/git_machete/utils.py b/git_machete/utils.py
index 5b13772..6c40769 100644
--- a/git_machete/utils.py
+++ b/git_machete/utils.py
@@ -4,6 +4,7 @@ import os
 import re
 import subprocess
 import sys
+import time
 from typing import (Any, Callable, Dict, Iterable, List, NamedTuple, Optional,
                     Set, Tuple, TypeVar)
 
@@ -19,6 +20,7 @@ current_directory_confirmed_to_exist: bool = False
 
 ascii_only: bool = not sys.stdout.isatty()
 debug_mode: bool = False
+measure_command_time: bool = os.environ.get('GIT_MACHETE_MEASURE_COMMAND_TIME') == 'true'  # undocumented, internal
 verbose_mode: bool = False
 
 # https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/
@@ -129,12 +131,24 @@ def run_cmd(cmd: str, *args: str, cwd: Optional[str] = None, env: Optional[Dict[
     chdir_upwards_until_current_directory_exists()
 
     flat_cmd: str = get_cmd_shell_repr(cmd, *args, env=env)
+
+    def print_command(cmd: str) -> None:
+        if measure_command_time:  # pragma: no cover
+            print(cmd + " ... ", file=sys.stderr, end='', flush=True)
+        else:
+            print(cmd, file=sys.stderr)
+
     if debug_mode:
-        print(bold(f">>> {flat_cmd}"), file=sys.stderr)
+        print_command(bold(f">>> {flat_cmd}"))
     elif verbose_mode:
-        print(flat_cmd, file=sys.stderr)
+        print_command(flat_cmd)
 
+    start = time.time()
     exit_code: int = _run_cmd(cmd, *args, cwd=cwd, env=env)
+    if measure_command_time:  # pragma: no cover
+        end = time.time()
+        elapsed_ms = int((end - start) * 1e3)
+        print(f"{elapsed_ms} ms")
 
     # Let's defensively assume that every command executed via run_cmd
     # (but not via popen_cmd) can make the current directory disappear.
@@ -187,12 +201,24 @@ def popen_cmd(cmd: str, *args: str, cwd: Optional[str] = None,
     chdir_upwards_until_current_directory_exists()
 
     flat_cmd = get_cmd_shell_repr(cmd, *args, env=env)
+
+    def print_command(cmd: str) -> None:
+        if measure_command_time:  # pragma: no cover
+            print(cmd + " ... ", file=sys.stderr, end='', flush=True)
+        else:
+            print(cmd, file=sys.stderr)
+
     if debug_mode:
-        print(bold(f">>> {flat_cmd}"), file=sys.stderr)
+        print_command(bold(f">>> {flat_cmd}"))
     elif verbose_mode:
-        print(flat_cmd, file=sys.stderr)
+        print_command(flat_cmd)
 
+    start = time.time()
     exit_code, stdout, stderr = result = _popen_cmd(cmd, *args, cwd=cwd, env=env)
+    if measure_command_time:  # pragma: no cover
+        end = time.time()
+        elapsed_ms = int((end - start) * 1e3)
+        print(f"{elapsed_ms} ms")
 
     # GitHub tokens are likely to appear e.g. in the output of `git config -l`:
     # `https://<TOKEN>@github.com/org/repo.git` is a supported URL format for git remotes.


Test patch:
diff --git a/tests/test_clean.py b/tests/test_clean.py
index d7d2703..d143171 100644
--- a/tests/test_clean.py
+++ b/tests/test_clean.py
@@ -75,7 +75,7 @@ class TestClean(BaseTest):
         assert_success(
             ["clean", "--checkout-my-github-prs"],
             """
-            Checking for open GitHub PRs... Warn: Could not determine current user name, please check that the GitHub API token provided by one of the:
+            Warn: Could not determine current user name, please check that the GitHub API token provided by one of the:
             \t1. GITHUB_TOKEN environment variable
             \t2. Content of the ~/.github-token file
             \t3. Current auth token from the gh GitHub CLI
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index 0b92bb8..e3158d4 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -27,6 +27,7 @@ class TestGitHubCreatePR(BaseTest):
         self.patch_symbol(mocker, 'builtins.input', mock_input_returning_y)
         self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake)
         self.patch_symbol(mocker, 'git_machete.github.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.utils.get_current_date', lambda: '2023-12-31')
         github_api_state = self.github_api_state_for_test_create_pr()
         self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(github_api_state))
 
@@ -143,6 +144,7 @@ class TestGitHubCreatePR(BaseTest):
 
             Fetching origin...
             Creating a draft PR from chore/fields to ignore-trailing... OK, see www.github.com
+            Checking for open GitHub PRs (to determine PR chain)... OK
             Updating description of PR #5 to include the chain of PRs... OK
             Setting milestone of PR #5 to #42... OK
             Adding github_user as assignee to PR #5... OK
@@ -157,7 +159,7 @@ class TestGitHubCreatePR(BaseTest):
 
             # Based on PR #3
 
-            ## Full chain of PRs as of 2024-02-04
+            ## Full chain of PRs as of 2023-12-31
 
             * PR #5: `chore/fields` ➔ `ignore-trailing`
             * PR #3: `ignore-trailing` ➔ `hotfix/add-trigger`
@@ -455,6 +457,7 @@ 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, 'git_machete.utils.get_current_date', lambda: '2023-12-31')
         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))
 
@@ -562,6 +565,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Fetching origin_1...
         Creating a PR from feature to branch-1... OK, see www.github.com
+        Checking for open GitHub PRs (to determine PR chain)... OK
         Updating description of PR #16 to include the chain of PRs... OK
         """  # noqa: E501
 
@@ -578,7 +582,7 @@ class TestGitHubCreatePR(BaseTest):
 
             # Based on PR #15
 
-            ## Full chain of PRs as of 2024-02-04
+            ## Full chain of PRs as of 2023-12-31
 
             * PR #16: `feature` ➔ `branch-1`
             * PR #15: `branch-1` ➔ `root`
@@ -611,6 +615,7 @@ class TestGitHubCreatePR(BaseTest):
         Added branch feature_1 onto feature
         Fetching origin_2...
         Creating a PR from feature_1 to feature... OK, see www.github.com
+        Checking for open GitHub PRs (to determine PR chain)... OK
         Updating description of PR #17 to include the chain of PRs... OK
         """
         assert_success(
@@ -648,6 +653,7 @@ class TestGitHubCreatePR(BaseTest):
 
         Fetching origin_1...
         Creating a PR from feature_2 to feature... OK, see www.github.com
+        Checking for open GitHub PRs (to determine PR chain)... OK
         Updating description of PR #18 to include the chain of PRs... OK
         """
         assert_success(
@@ -669,6 +675,7 @@ class TestGitHubCreatePR(BaseTest):
         Added branch feature_3 onto feature_2
         Fetching origin_1...
         Creating a PR from feature_3 to feature_2... OK, see www.github.com
+        Checking for open GitHub PRs (to determine PR chain)... OK
         Updating description of PR #19 to include the chain of PRs... OK
         """  # noqa: E501
         assert_success(
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index 4c20057..50818c3 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -16,7 +16,7 @@ class TestGitHubRetargetPR(BaseTest):
     def github_api_state_for_test_retarget_pr() -> MockGitHubAPIState:
         return MockGitHubAPIState(
             mock_pr_json(head='feature', base='master', number=15),
-            mock_pr_json(head='feature_1', base='master', number=20, body='# Summary\n\n'),
+            mock_pr_json(head='feature_1', base='master', number=20, body='# Based on PR #10\n\n# Summary'),
             mock_pr_json(head='feature_2', base='master', number=25, body=None),
             mock_pr_json(head='feature_3', base='master', number=30),
             mock_pr_json(head='feature_4', base='feature', number=35),
@@ -257,6 +257,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #20 has been switched to feature\n'
+            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
             'Description of PR #20 has been updated\n'
         )
         pr20 = github_api_state.get_pull_by_number(20)
@@ -274,9 +275,7 @@ class TestGitHubRetargetPR(BaseTest):
 
             <!-- end git-machete generated -->
 
-            # Summary
-
-        ''')[1:]
+            # Summary''')[1:]
 
         # branch feature_2 is not present in any of the remotes
         (
@@ -306,6 +305,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #25 has been switched to feature\n'
+            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
             'Description of PR #25 has been updated\n'
         )
         pr25 = github_api_state.get_pull_by_number(25)
@@ -346,6 +346,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #30 has been switched to feature_2\n'
+            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
             'Description of PR #30 has been updated\n'
         )
         pr30 = github_api_state.get_pull_by_number(30)
@@ -380,6 +381,7 @@ class TestGitHubRetargetPR(BaseTest):
         assert_success(
             ['github', 'retarget-pr'],
             'Base branch of PR #30 has been switched to feature\n'
+            'Checking for open GitHub PRs (to determine PR chain)... OK\n'
             'Description of PR #30 has been updated\n'
         )
         pr30 = github_api_state.get_pull_by_number(30)