Просмотр исходного кода

Add support for GIT_REFLOG_ACTION environment variable

Fixes #1811
Jelmer Vernooij 2 месяцев назад
Родитель
Сommit
5e1a709383
3 измененных файлов с 148 добавлено и 6 удалено
  1. 3 0
      NEWS
  2. 67 6
      dulwich/porcelain.py
  3. 78 0
      tests/test_porcelain.py

+ 3 - 0
NEWS

@@ -10,6 +10,9 @@
    environments like GitHub Actions. More specific URL configurations
    environments like GitHub Actions. More specific URL configurations
    override less specific ones.  (Jelmer Vernooij, #882)
    override less specific ones.  (Jelmer Vernooij, #882)
 
 
+ * Add support for ``GIT_REFLOG_ACTION`` environment variable in porcelain
+   functions. (Jelmer Vernooij, #1811)
+
  * Add support for namespace isolation via ``NamespacedRefsContainer``.
  * Add support for namespace isolation via ``NamespacedRefsContainer``.
    Implements Git's namespace feature for isolating refs within a single
    Implements Git's namespace feature for isolating refs within a single
    repository using the ``refs/namespaces/`` prefix. (Jelmer Vernooij, #1809)
    repository using the ``refs/namespaces/`` prefix. (Jelmer Vernooij, #1809)

+ 67 - 6
dulwich/porcelain.py

@@ -422,6 +422,31 @@ def _noop_context_manager(obj: T) -> Iterator[T]:
     yield obj
     yield obj
 
 
 
 
+def _get_reflog_message(
+    default_message: bytes, explicit_message: Optional[bytes] = None
+) -> bytes:
+    """Get reflog message, checking GIT_REFLOG_ACTION environment variable.
+
+    Args:
+      default_message: Default message to use if no explicit message or env var
+      explicit_message: Explicit message passed as argument (takes precedence)
+
+    Returns:
+      The reflog message to use, with priority:
+        1. explicit_message if provided
+        2. GIT_REFLOG_ACTION environment variable if set
+        3. default_message otherwise
+    """
+    if explicit_message is not None:
+        return explicit_message
+
+    env_action = os.environ.get("GIT_REFLOG_ACTION")
+    if env_action is not None:
+        return env_action.encode("utf-8")
+
+    return default_message
+
+
 @overload
 @overload
 def open_repo_closing(path_or_repo: T) -> AbstractContextManager[T]: ...
 def open_repo_closing(path_or_repo: T) -> AbstractContextManager[T]: ...
 
 
@@ -844,8 +869,23 @@ def commit(
                 merge_heads=merge_heads,
                 merge_heads=merge_heads,
                 ref=None,
                 ref=None,
             )
             )
-            # Update HEAD to point to the new commit
-            r.refs[b"HEAD"] = commit_sha
+            # Update HEAD to point to the new commit with reflog message
+            try:
+                old_head = r.refs[b"HEAD"]
+            except KeyError:
+                old_head = None
+
+            # Get the actual commit message from the created commit
+            commit_obj = r[commit_sha]
+            assert isinstance(commit_obj, Commit)
+            commit_message = commit_obj.message
+            default_message = b"commit (amend): " + commit_message
+            # Truncate message if too long for reflog
+            if len(default_message) > 100:
+                default_message = default_message[:97] + b"..."
+            reflog_message = _get_reflog_message(default_message)
+
+            r.refs.set_if_equals(b"HEAD", old_head, commit_sha, message=reflog_message)
             return commit_sha
             return commit_sha
         else:
         else:
             return r.get_worktree().commit(
             return r.get_worktree().commit(
@@ -2736,7 +2776,27 @@ def reset(
 
 
         # Update HEAD to point to the target commit
         # Update HEAD to point to the target commit
         if target_commit is not None:
         if target_commit is not None:
-            r.refs[b"HEAD"] = target_commit.id
+            # Get the current HEAD value for set_if_equals
+            try:
+                old_head = r.refs[b"HEAD"]
+            except KeyError:
+                old_head = None
+
+            # Create reflog message
+            treeish_str = (
+                treeish.decode("utf-8")
+                if isinstance(treeish, bytes)
+                else str(treeish)
+                if not isinstance(treeish, (Commit, Tree, Tag))
+                else target_commit.id.hex()
+            )
+            default_message = f"reset: moving to {treeish_str}".encode()
+            reflog_message = _get_reflog_message(default_message)
+
+            # Update HEAD with reflog message
+            r.refs.set_if_equals(
+                b"HEAD", old_head, target_commit.id, message=reflog_message
+            )
 
 
         if mode == "soft":
         if mode == "soft":
             # Soft reset: only update HEAD, leave index and working tree unchanged
             # Soft reset: only update HEAD, leave index and working tree unchanged
@@ -3850,11 +3910,12 @@ def branch_create(
 
 
         object = parse_object(r, objectish)
         object = parse_object(r, objectish)
         refname = _make_branch_ref(name)
         refname = _make_branch_ref(name)
-        ref_message = (
+        default_message = (
             b"branch: Created from " + original_objectish.encode(DEFAULT_ENCODING)
             b"branch: Created from " + original_objectish.encode(DEFAULT_ENCODING)
             if isinstance(original_objectish, str)
             if isinstance(original_objectish, str)
             else b"branch: Created from " + original_objectish
             else b"branch: Created from " + original_objectish
         )
         )
+        ref_message = _get_reflog_message(default_message)
         if force:
         if force:
             r.refs.set_if_equals(refname, None, object.id, message=ref_message)
             r.refs.set_if_equals(refname, None, object.id, message=ref_message)
         else:
         else:
@@ -4249,8 +4310,8 @@ def fetch(
     """
     """
     with open_repo_closing(repo) as r:
     with open_repo_closing(repo) as r:
         (remote_name, remote_location) = get_remote_repo(r, remote_location)
         (remote_name, remote_location) = get_remote_repo(r, remote_location)
-        if message is None:
-            message = b"fetch: from " + remote_location.encode(DEFAULT_ENCODING)
+        default_message = b"fetch: from " + remote_location.encode(DEFAULT_ENCODING)
+        message = _get_reflog_message(default_message, message)
         client, path = get_transport_and_path(
         client, path = get_transport_and_path(
             remote_location,
             remote_location,
             config=r.get_config_stack(),
             config=r.get_config_stack(),

+ 78 - 0
tests/test_porcelain.py

@@ -10827,3 +10827,81 @@ class ReplaceDeleteTests(PorcelainTestCase):
         # Try to delete a non-existent replacement
         # Try to delete a non-existent replacement
         with self.assertRaises(KeyError):
         with self.assertRaises(KeyError):
             porcelain.replace_delete(self.repo, c1.id)
             porcelain.replace_delete(self.repo, c1.id)
+
+
+class GitReflogActionTests(PorcelainTestCase):
+    """Tests for GIT_REFLOG_ACTION environment variable support."""
+
+    def test_reset_with_git_reflog_action(self) -> None:
+        """Test that reset respects GIT_REFLOG_ACTION environment variable."""
+        [c1, c2] = build_commit_graph(self.repo.object_store, [[1], [2]])
+        self.repo.refs[b"HEAD"] = c2.id
+
+        # Set GIT_REFLOG_ACTION environment variable
+        self.overrideEnv("GIT_REFLOG_ACTION", "custom reset action")
+
+        # Reset to c1
+        porcelain.reset(self.repo, "hard", c1.id.decode())
+
+        # Check reflog message - HEAD is a symref to refs/heads/master
+        entries = list(porcelain.reflog(self.repo_path, b"refs/heads/master"))
+        self.assertEqual(1, len(entries))
+        self.assertEqual(b"custom reset action", entries[0].message)
+
+    def test_commit_amend_with_git_reflog_action(self) -> None:
+        """Test that commit --amend respects GIT_REFLOG_ACTION environment variable."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo.refs[b"HEAD"] = c1.id
+
+        # Set GIT_REFLOG_ACTION environment variable
+        self.overrideEnv("GIT_REFLOG_ACTION", "custom amend action")
+
+        # Amend the commit
+        porcelain.commit(self.repo, b"amended message", amend=True)
+
+        # Check reflog message - HEAD is a symref to refs/heads/master
+        entries = list(porcelain.reflog(self.repo_path, b"refs/heads/master"))
+        self.assertEqual(1, len(entries))
+        self.assertEqual(b"custom amend action", entries[0].message)
+
+    def test_branch_create_with_git_reflog_action(self) -> None:
+        """Test that branch_create respects GIT_REFLOG_ACTION environment variable."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo.refs[b"HEAD"] = c1.id
+
+        # Set GIT_REFLOG_ACTION environment variable
+        self.overrideEnv("GIT_REFLOG_ACTION", "custom branch action")
+
+        # Create a new branch
+        porcelain.branch_create(self.repo, b"test-branch")
+
+        # Check reflog message
+        entries = list(porcelain.reflog(self.repo_path, b"refs/heads/test-branch"))
+        self.assertEqual(1, len(entries))
+        self.assertEqual(b"custom branch action", entries[0].message)
+
+    def test_reset_without_git_reflog_action(self) -> None:
+        """Test that reset uses default message when GIT_REFLOG_ACTION is not set."""
+        [c1, c2] = build_commit_graph(self.repo.object_store, [[1], [2]])
+        self.repo.refs[b"HEAD"] = c2.id
+
+        # Reset to c1 without GIT_REFLOG_ACTION
+        porcelain.reset(self.repo, "hard", c1.id.decode())
+
+        # Check reflog message contains default format - HEAD is a symref to refs/heads/master
+        entries = list(porcelain.reflog(self.repo_path, b"refs/heads/master"))
+        self.assertEqual(1, len(entries))
+        self.assertTrue(entries[0].message.startswith(b"reset: moving to"))
+
+    def test_branch_create_without_git_reflog_action(self) -> None:
+        """Test that branch_create uses default message when GIT_REFLOG_ACTION is not set."""
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo.refs[b"HEAD"] = c1.id
+
+        # Create a new branch without GIT_REFLOG_ACTION
+        porcelain.branch_create(self.repo, b"test-branch")
+
+        # Check reflog message contains default format
+        entries = list(porcelain.reflog(self.repo_path, b"refs/heads/test-branch"))
+        self.assertEqual(1, len(entries))
+        self.assertTrue(entries[0].message.startswith(b"branch: Created from"))