Repository: VirtusLab/git-machete
Pull request #1280 (2024-07-05 14:09:40)

Limit log traversal for fork point to max 100 commits


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index f858029..92ff707 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,10 @@
 # Release notes
 
+## New in git-machete 3.26.3
+
+- improved: performance of listing commits for red-edge branches on large repos
+- improved: message in case of missing `.git/machete` file suggests to use `git machete git{hub,lab} checkout-{prs,mrs}`
+
 ## New in git-machete 3.26.2
 
 - fixed: parsing of multiline git config keys (reported by @saveman71)
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index e680113..30be20f 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" "Jul 03, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Jul 05, 2024" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.26.2
+git-machete \- git-machete 3.26.3
 .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 e8fb4e0..e8867b7 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.26.2'
+__version__ = '3.26.3'
diff --git a/git_machete/client.py b/git_machete/client.py
index abe6ebf..dc7dead 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -5,6 +5,7 @@ import os
 import shlex
 import shutil
 import sys
+import textwrap
 from collections import OrderedDict
 from enum import Enum, auto
 from typing import Callable, Dict, Iterator, List, Optional, Tuple
@@ -15,9 +16,11 @@ from .code_hosting import (CodeHostingClient, CodeHostingSpec,
                            OrganizationAndRepository,
                            OrganizationAndRepositoryAndRemote, PullRequest,
                            is_matching_remote_url)
-from .constants import (DISCOVER_DEFAULT_FRESH_BRANCH_COUNT, PICK_FIRST_ROOT,
-                        PICK_LAST_ROOT, GitFormatPatterns,
-                        SquashMergeDetection, SyncToRemoteStatuses)
+from .constants import (DISCOVER_DEFAULT_FRESH_BRANCH_COUNT,
+                        INITIAL_COMMIT_COUNT_FOR_LOG, PICK_FIRST_ROOT,
+                        PICK_LAST_ROOT, TOTAL_COMMIT_COUNT_FOR_LOG,
+                        GitFormatPatterns, SquashMergeDetection,
+                        SyncToRemoteStatuses)
 from .exceptions import (InteractionStopped, MacheteException,
                          UnexpectedMacheteException)
 from .git_operations import (HEAD, AnyBranchName, AnyRevision, BranchPair,
@@ -88,9 +91,12 @@ class MacheteClient:
 
     def __raise_no_branches_error(self) -> None:
         raise MacheteException(
-            f"No branches listed in {self.__branch_layout_file_path}; use `git "
-            f"machete discover` or `git machete edit`, or edit"
-            f" {self.__branch_layout_file_path} manually.")
+            textwrap.dedent(f"""
+                No branches listed in {self.__branch_layout_file_path}. Consider one of:
+                * `git machete discover`
+                * `git machete edit` or edit {self.__branch_layout_file_path} manually
+                * `git machete github checkout-prs --mine`
+                * `git machete gitlab checkout-mrs --mine`"""[1:]))
 
     def read_branch_layout_file(self, perform_interactive_slide_out: bool, verify_branches: bool = True) -> None:
         with open(self.__branch_layout_file_path) as file:
@@ -1759,7 +1765,9 @@ class MacheteClient:
         if not branch_full_hash:
             return
 
-        for hash in self.__git.spoonfeed_log_hashes(branch_full_hash):
+        for hash in self.__git.spoonfeed_log_hashes(branch_full_hash,
+                                                    initial_count=INITIAL_COMMIT_COUNT_FOR_LOG,
+                                                    total_count=TOTAL_COMMIT_COUNT_FOR_LOG):
             if hash in self.__branch_pairs_by_hash_in_reflog:
                 # The entries must be sorted by lb_or_rb to make sure the
                 # upstream inference is deterministic (and does not depend on the
diff --git a/git_machete/constants.py b/git_machete/constants.py
index 54c6156..b5cf433 100644
--- a/git_machete/constants.py
+++ b/git_machete/constants.py
@@ -5,7 +5,8 @@ from git_machete.exceptions import MacheteException
 
 DISCOVER_DEFAULT_FRESH_BRANCH_COUNT = 10
 MAX_COMMITS_FOR_SQUASH_MERGE_DETECTION = 1000
-MAX_COUNT_FOR_INITIAL_LOG = 10
+INITIAL_COMMIT_COUNT_FOR_LOG = 10
+TOTAL_COMMIT_COUNT_FOR_LOG = 100
 
 PICK_FIRST_ROOT: int = 0
 PICK_LAST_ROOT: int = -1
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index ac1c811..f0fe778 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -9,8 +9,7 @@ from typing import (Any, Dict, Iterator, List, Match, NamedTuple, Optional,
 
 from . import utils
 from .constants import (MAX_COMMITS_FOR_SQUASH_MERGE_DETECTION,
-                        MAX_COUNT_FOR_INITIAL_LOG, GitFormatPatterns,
-                        SyncToRemoteStatuses)
+                        GitFormatPatterns, SyncToRemoteStatuses)
 from .exceptions import UnderlyingGitException, UnexpectedMacheteException
 from .utils import (AnsiEscapeCodes, CommandResult, colored, debug, fmt,
                     hex_repr)
@@ -598,18 +597,20 @@ class GitContext:
         return list(map(FullCommitHash.of, utils.get_non_empty_lines(self._popen_git("log", *opts).stdout)))
 
     # Since getting the full history of a branch can be an expensive operation for large repositories
-    # (compared to all other underlying git operations), there's a simple optimization in place:
-    # we first fetch only a couple of first commits in the history, and only fetch the rest if needed.
-    def spoonfeed_log_hashes(self, branch_full_hash: FullCommitHash) -> Iterator[FullCommitHash]:
+    # (compared to all other underlying git operations), there are two optimizations in place:
+    # 1. we first fetch only a couple of first commits in the history,
+    # 2. if these first commits aren't enough for the purpose, we fetch more, but only up to the hard limit,
+    #    to avoid time-unbounded operations on really large repos.
+    def spoonfeed_log_hashes(self, branch_full_hash: FullCommitHash, initial_count: int, total_count: int) -> Iterator[FullCommitHash]:
         if branch_full_hash not in self.__initial_log_hashes_cached:
-            self.__initial_log_hashes_cached[branch_full_hash] = self.__get_log_hashes(branch_full_hash,
-                                                                                       max_count=MAX_COUNT_FOR_INITIAL_LOG)
+            self.__initial_log_hashes_cached[branch_full_hash] = \
+                self.__get_log_hashes(branch_full_hash, max_count=initial_count)
         for hash in self.__initial_log_hashes_cached[branch_full_hash]:
             yield FullCommitHash.of(hash)
 
         if branch_full_hash not in self.__remaining_log_hashes_cached:
-            self.__remaining_log_hashes_cached[branch_full_hash] = self.__get_log_hashes(branch_full_hash,
-                                                                                         max_count=None)[MAX_COUNT_FOR_INITIAL_LOG:]
+            self.__remaining_log_hashes_cached[branch_full_hash] = \
+                self.__get_log_hashes(branch_full_hash, max_count=total_count)[initial_count:]
         for hash in self.__remaining_log_hashes_cached[branch_full_hash]:
             yield FullCommitHash.of(hash)
 


Test patch:
diff --git a/tests/test_go.py b/tests/test_go.py
index 9db14aa..60a8914 100644
--- a/tests/test_go.py
+++ b/tests/test_go.py
@@ -456,8 +456,10 @@ class TestGo(BaseTest):
              "the root of the current branch.")
 
     def test_go_root_no_branches(self) -> None:
-        assert_failure(
-            ["g", "root"],
-            "No branches listed in .git/machete; use git machete discover "
-            "or git machete edit, or edit .git/machete manually."
-        )
+        expected_error_message = """
+          No branches listed in .git/machete. Consider one of:
+          * git machete discover
+          * git machete edit or edit .git/machete manually
+          * git machete github checkout-prs --mine
+          * git machete gitlab checkout-mrs --mine"""
+        assert_failure(["g", "root"], expected_error_message)
diff --git a/tests/test_traverse.py b/tests/test_traverse.py
index c573e03..0f984f8 100644
--- a/tests/test_traverse.py
+++ b/tests/test_traverse.py
@@ -981,8 +981,12 @@ class TestTraverse(BaseTest):
 
     def test_traverse_no_managed_branches(self) -> None:
 
-        expected_error_message = "No branches listed in .git/machete; " \
-                                 "use git machete discover or git machete edit, or edit .git/machete manually."
+        expected_error_message = """
+          No branches listed in .git/machete. Consider one of:
+          * git machete discover
+          * git machete edit or edit .git/machete manually
+          * git machete github checkout-prs --mine
+          * git machete gitlab checkout-mrs --mine"""
         assert_failure(["traverse"], expected_error_message)
 
     def test_traverse_invalid_flag_values(self) -> None: