浏览代码

Add basic reflog command

Jelmer Vernooij 2 月之前
父节点
当前提交
ff9d75f00b
共有 7 个文件被更改,包括 398 次插入32 次删除
  1. 31 29
      NEWS
  2. 31 0
      dulwich/cli.py
  3. 31 0
      dulwich/porcelain.py
  4. 26 1
      dulwich/reflog.py
  5. 18 0
      dulwich/repo.py
  6. 138 1
      tests/test_porcelain.py
  7. 123 1
      tests/test_reflog.py

+ 31 - 29
NEWS

@@ -1,5 +1,7 @@
 0.23.3	UNRELEASED
 0.23.3	UNRELEASED
 
 
+ * Add ``reflog`` command in porcelain. (Jelmer Vernooij)
+
 0.23.2	2025-07-07
 0.23.2	2025-07-07
 
 
  * Print deprecations on usage, not import.
  * Print deprecations on usage, not import.
@@ -19,7 +21,7 @@
    functionality works as expected.
    functionality works as expected.
    (Jelmer Vernooij, #780)
    (Jelmer Vernooij, #780)
 
 
- * Add porcelain submodule commands: ``submodule_update``, ``submodule_add`` 
+ * Add porcelain submodule commands: ``submodule_update``, ``submodule_add``g
    CLI command, and ``submodule_update`` CLI command. Add ``--recurse-submodules``
    CLI command, and ``submodule_update`` CLI command. Add ``--recurse-submodules``
    option to ``clone`` command. (#506, Jelmer Vernooij)
    option to ``clone`` command. (#506, Jelmer Vernooij)
 
 
@@ -559,7 +561,7 @@
  * Build 32 bit wheels for Windows.
  * Build 32 bit wheels for Windows.
    (Benjamin Parzella)
    (Benjamin Parzella)
 
 
- * tests: Ignore errors when deleting GNUPG 
+ * tests: Ignore errors when deleting GNUPGg
    home directory. Fixes spurious errors racing
    home directory. Fixes spurious errors racing
    gnupg-agent. Thanks, Matěj Cepl. Fixes #1000
    gnupg-agent. Thanks, Matěj Cepl. Fixes #1000
 
 
@@ -2150,7 +2152,7 @@ FEATURES
 
 
  IMPROVEMENTS
  IMPROVEMENTS
 
 
-  * Support passing a single revision to BaseRepo.get_walker() rather than a list of revisions. 
+  * Support passing a single revision to BaseRepo.get_walker() rather than a list of revisions.g
     (Alberto Ruiz)
     (Alberto Ruiz)
 
 
   * Add `Repo.get_description` method. (Jelmer Vernooij)
   * Add `Repo.get_description` method. (Jelmer Vernooij)
@@ -2678,7 +2680,7 @@ FEATURES
 
 
   * Tweak server handler injection. (Dave Borowitz)
   * Tweak server handler injection. (Dave Borowitz)
 
 
-  * PackIndex1 and PackIndex2 now subclass FilePackIndex, which is 
+  * PackIndex1 and PackIndex2 now subclass FilePackIndex, which isg
     itself a subclass of PackIndex. (Jelmer Vernooij)
     itself a subclass of PackIndex. (Jelmer Vernooij)
 
 
  DOCUMENTATION
  DOCUMENTATION
@@ -2713,7 +2715,7 @@ FEATURES
   * The GitClient interface has been cleaned up and instances are now reusable.
   * The GitClient interface has been cleaned up and instances are now reusable.
     (Augie Fackler)
     (Augie Fackler)
 
 
-  * Allow overriding paths to executables in GitSSHClient. 
+  * Allow overriding paths to executables in GitSSHClient.g
     (Ross Light, Jelmer Vernooij, #585204)
     (Ross Light, Jelmer Vernooij, #585204)
 
 
   * Add PackBasedObjectStore.pack_loose_objects(). (Jelmer Vernooij)
   * Add PackBasedObjectStore.pack_loose_objects(). (Jelmer Vernooij)
@@ -2758,11 +2760,11 @@ FEATURES
 note: This list is most likely incomplete for 0.6.0.
 note: This list is most likely incomplete for 0.6.0.
 
 
  BUG FIXES
  BUG FIXES
- 
+g
   * Fix ReceivePackHandler to disallow removing refs without delete-refs.
   * Fix ReceivePackHandler to disallow removing refs without delete-refs.
     (Dave Borowitz)
     (Dave Borowitz)
 
 
-  * Deal with capabilities required by the client, even if they 
+  * Deal with capabilities required by the client, even if theyg
     can not be disabled in the server. (Dave Borowitz)
     can not be disabled in the server. (Dave Borowitz)
 
 
   * Fix trailing newlines in generated patch files.
   * Fix trailing newlines in generated patch files.
@@ -2781,27 +2783,27 @@ note: This list is most likely incomplete for 0.6.0.
 
 
   * Fix fetch if no progress function is specified. (Augie Fackler)
   * Fix fetch if no progress function is specified. (Augie Fackler)
 
 
-  * Allow double-staging of files that are deleted in the index. 
+  * Allow double-staging of files that are deleted in the index.g
     (Dave Borowitz)
     (Dave Borowitz)
 
 
   * Fix RefsContainer.add_if_new to support dangling symrefs.
   * Fix RefsContainer.add_if_new to support dangling symrefs.
     (Dave Borowitz)
     (Dave Borowitz)
 
 
-  * Non-existent index files in non-bare repositories are now treated as 
+  * Non-existent index files in non-bare repositories are now treated asg
     empty. (Dave Borowitz)
     empty. (Dave Borowitz)
 
 
-  * Always update ShaFile.id when the contents of the object get changed. 
+  * Always update ShaFile.id when the contents of the object get changed.g
     (Jelmer Vernooij)
     (Jelmer Vernooij)
 
 
   * Various Python2.4-compatibility fixes. (Dave Borowitz)
   * Various Python2.4-compatibility fixes. (Dave Borowitz)
 
 
   * Fix thin pack handling. (Dave Borowitz)
   * Fix thin pack handling. (Dave Borowitz)
- 
+g
  FEATURES
  FEATURES
 
 
   * Add include-tag capability to server. (Dave Borowitz)
   * Add include-tag capability to server. (Dave Borowitz)
 
 
-  * New dulwich.fastexport module that can generate fastexport 
+  * New dulwich.fastexport module that can generate fastexportg
     streams. (Jelmer Vernooij)
     streams. (Jelmer Vernooij)
 
 
   * Implemented BaseRepo.__contains__. (Jelmer Vernooij)
   * Implemented BaseRepo.__contains__. (Jelmer Vernooij)
@@ -2818,7 +2820,7 @@ note: This list is most likely incomplete for 0.6.0.
 
 
   * Add various tests for the use of non-bare repositories. (Dave Borowitz)
   * Add various tests for the use of non-bare repositories. (Dave Borowitz)
 
 
-  * Cope with diffstat not being available on all platforms. 
+  * Cope with diffstat not being available on all platforms.g
     (Tay Ray Chuan, Jelmer Vernooij)
     (Tay Ray Chuan, Jelmer Vernooij)
 
 
   * Add make_object and make_commit convenience functions to test utils.
   * Add make_object and make_commit convenience functions to test utils.
@@ -2826,7 +2828,7 @@ note: This list is most likely incomplete for 0.6.0.
 
 
  API BREAKAGES
  API BREAKAGES
 
 
-  * The 'committer' and 'message' arguments to Repo.do_commit() have 
+  * The 'committer' and 'message' arguments to Repo.do_commit() haveg
     been swapped. 'committer' is now optional. (Jelmer Vernooij)
     been swapped. 'committer' is now optional. (Jelmer Vernooij)
 
 
   * Repo.get_blob, Repo.commit, Repo.tag and Repo.tree are now deprecated.
   * Repo.get_blob, Repo.commit, Repo.tag and Repo.tree are now deprecated.
@@ -2837,8 +2839,8 @@ note: This list is most likely incomplete for 0.6.0.
 
 
  API CHANGES
  API CHANGES
 
 
-  * The primary serialization APIs in dulwich.objects now work 
-    with chunks of strings rather than with full-text strings. 
+  * The primary serialization APIs in dulwich.objects now workg
+    with chunks of strings rather than with full-text strings.g
     (Jelmer Vernooij)
     (Jelmer Vernooij)
 
 
 0.5.02010-03-03
 0.5.02010-03-03
@@ -2852,7 +2854,7 @@ note: This list is most likely incomplete for 0.6.0.
   * Rework server protocol to be smarter and interoperate with cgit client.
   * Rework server protocol to be smarter and interoperate with cgit client.
     (Dave Borowitz)
     (Dave Borowitz)
 
 
-  * Add a GitFile class that uses the same locking protocol for writes as 
+  * Add a GitFile class that uses the same locking protocol for writes asg
     cgit. (Dave Borowitz)
     cgit. (Dave Borowitz)
 
 
   * Cope with forward slashes correctly in the index on Windows.
   * Cope with forward slashes correctly in the index on Windows.
@@ -2860,14 +2862,14 @@ note: This list is most likely incomplete for 0.6.0.
 
 
  FEATURES
  FEATURES
 
 
-  * --pure option to setup.py to allow building/installing without the C 
+  * --pure option to setup.py to allow building/installing without the Cg
     extensions. (Hal Wine, Anatoly Techtonik, Jelmer Vernooij, #434326)
     extensions. (Hal Wine, Anatoly Techtonik, Jelmer Vernooij, #434326)
 
 
   * Implement Repo.get_config(). (Jelmer Vernooij, Augie Fackler)
   * Implement Repo.get_config(). (Jelmer Vernooij, Augie Fackler)
 
 
   * HTTP dumb and smart server. (Dave Borowitz)
   * HTTP dumb and smart server. (Dave Borowitz)
 
 
-  * Add abstract baseclass for Repo that does not require file system 
+  * Add abstract baseclass for Repo that does not require file systemg
     operations. (Dave Borowitz)
     operations. (Dave Borowitz)
 
 
 0.4.1	2010-01-03
 0.4.1	2010-01-03
@@ -2891,7 +2893,7 @@ note: This list is most likely incomplete for 0.6.0.
 
 
  API CHANGES
  API CHANGES
 
 
-  * dulwich.object_store.tree_lookup_path will now return the mode and 
+  * dulwich.object_store.tree_lookup_path will now return the mode andg
     sha of the object found rather than the object itself.
     sha of the object found rather than the object itself.
 
 
  BUG FIXES
  BUG FIXES
@@ -2913,7 +2915,7 @@ note: This list is most likely incomplete for 0.6.0.
   * Implement Tree.__len__()
   * Implement Tree.__len__()
 
 
  BUG FIXES
  BUG FIXES
-  
+ g
   * Check for 'objects' and 'refs' directories
   * Check for 'objects' and 'refs' directories
     when looking for a Git repository. (#380818)
     when looking for a Git repository. (#380818)
 
 
@@ -2922,7 +2924,7 @@ note: This list is most likely incomplete for 0.6.0.
  BUG FIXES
  BUG FIXES
 
 
   * Support the encoding field in Commits.
   * Support the encoding field in Commits.
-  
+ g
   * Some Windows compatibility fixes.
   * Some Windows compatibility fixes.
 
 
   * Fixed several issues in commit support.
   * Fixed several issues in commit support.
@@ -2935,31 +2937,31 @@ note: This list is most likely incomplete for 0.6.0.
 
 
  FEATURES
  FEATURES
 
 
-  * Implemented Repo.__getitem__, Repo.__setitem__ and Repo.__delitem__ to 
+  * Implemented Repo.__getitem__, Repo.__setitem__ and Repo.__delitem__ tog
     access content.
     access content.
 
 
  API CHANGES
  API CHANGES
 
 
-  * Removed Repo.set_ref, Repo.remove_ref, Repo.tags, Repo.get_refs and 
+  * Removed Repo.set_ref, Repo.remove_ref, Repo.tags, Repo.get_refs andg
     Repo.heads in favor of Repo.refs, a dictionary-like object for accessing
     Repo.heads in favor of Repo.refs, a dictionary-like object for accessing
     refs.
     refs.
 
 
  BUG FIXES
  BUG FIXES
 
 
-  * Removed import of 'sha' module in objects.py, which was causing 
+  * Removed import of 'sha' module in objects.py, which was causingg
     deprecation warnings on Python 2.6.
     deprecation warnings on Python 2.6.
 
 
 0.3.0	2009-05-10
 0.3.0	2009-05-10
 
 
  FEATURES
  FEATURES
 
 
-  * A new function 'commit_tree' has been added that can commit a tree 
+  * A new function 'commit_tree' has been added that can commit a treeg
     based on an index.
     based on an index.
 
 
  BUG FIXES
  BUG FIXES
 
 
   * The memory usage when generating indexes has been significantly reduced.
   * The memory usage when generating indexes has been significantly reduced.
- 
+g
   * A memory leak in the C implementation of parse_tree has been fixed.
   * A memory leak in the C implementation of parse_tree has been fixed.
 
 
   * The send-pack smart server command now works. (Thanks Scott Chacon)
   * The send-pack smart server command now works. (Thanks Scott Chacon)
@@ -2980,7 +2982,7 @@ note: This list is most likely incomplete for 0.6.0.
 
 
   * Support for activity reporting in smart protocol client.
   * Support for activity reporting in smart protocol client.
 
 
-  * Optional C extensions for better performance in a couple of 
+  * Optional C extensions for better performance in a couple ofg
     places that are performance-critical.
     places that are performance-critical.
 
 
 0.1.1	2009-03-13
 0.1.1	2009-03-13
@@ -2989,7 +2991,7 @@ note: This list is most likely incomplete for 0.6.0.
 
 
   * Fixed regression in Repo.find_missing_objects()
   * Fixed regression in Repo.find_missing_objects()
 
 
-  * Don't fetch ^{} objects from remote hosts, as requesting them 
+  * Don't fetch ^{} objects from remote hosts, as requesting themg
     causes a hangup.
     causes a hangup.
 
 
   * Always write pack to disk completely before calculating checksum.
   * Always write pack to disk completely before calculating checksum.

+ 31 - 0
dulwich/cli.py

@@ -533,6 +533,36 @@ class cmd_repack(Command):
         porcelain.repack(".")
         porcelain.repack(".")
 
 
 
 
+class cmd_reflog(Command):
+    def run(self, args) -> None:
+        parser = argparse.ArgumentParser()
+        parser.add_argument(
+            "ref", nargs="?", default="HEAD", help="Reference to show reflog for"
+        )
+        parser.add_argument(
+            "--all", action="store_true", help="Show reflogs for all refs"
+        )
+        args = parser.parse_args(args)
+
+        if args.all:
+            # Show reflogs for all refs
+            for ref_bytes, entry in porcelain.reflog(".", all=True):
+                ref_str = ref_bytes.decode("utf-8", "replace")
+                short_new = entry.new_sha[:8].decode("ascii")
+                print(
+                    f"{short_new} {ref_str}: {entry.message.decode('utf-8', 'replace')}"
+                )
+        else:
+            ref = args.ref.encode("utf-8") if isinstance(args.ref, str) else args.ref
+
+            for i, entry in enumerate(porcelain.reflog(".", ref)):
+                # Format similar to git reflog
+                short_new = entry.new_sha[:8].decode("ascii")
+                print(
+                    f"{short_new} {ref.decode('utf-8', 'replace')}@{{{i}}}: {entry.message.decode('utf-8', 'replace')}"
+                )
+
+
 class cmd_reset(Command):
 class cmd_reset(Command):
     def run(self, args) -> None:
     def run(self, args) -> None:
         parser = argparse.ArgumentParser()
         parser = argparse.ArgumentParser()
@@ -1968,6 +1998,7 @@ commands = {
     "push": cmd_push,
     "push": cmd_push,
     "rebase": cmd_rebase,
     "rebase": cmd_rebase,
     "receive-pack": cmd_receive_pack,
     "receive-pack": cmd_receive_pack,
+    "reflog": cmd_reflog,
     "remote": cmd_remote,
     "remote": cmd_remote,
     "repack": cmd_repack,
     "repack": cmd_repack,
     "reset": cmd_reset,
     "reset": cmd_reset,

+ 31 - 0
dulwich/porcelain.py

@@ -4544,3 +4544,34 @@ def bisect_replay(repo, log_file):
             log_content = log_file.read()
             log_content = log_file.read()
 
 
         state.replay(log_content)
         state.replay(log_content)
+
+
+def reflog(repo=".", ref=b"HEAD", all=False):
+    """Show reflog entries for a reference or all references.
+
+    Args:
+        repo: Path to repository or a Repo object
+        ref: Reference name (defaults to HEAD)
+        all: If True, show reflogs for all refs (ignores ref parameter)
+
+    Yields:
+        If all=False: ReflogEntry objects
+        If all=True: Tuples of (ref_name, ReflogEntry) for all refs with reflogs
+    """
+    import os
+
+    from .reflog import iter_reflogs
+
+    if isinstance(ref, str):
+        ref = ref.encode("utf-8")
+
+    with open_repo_closing(repo) as r:
+        if not all:
+            yield from r.read_reflog(ref)
+        else:
+            logs_dir = os.path.join(r.controldir(), "logs")
+            # Use iter_reflogs to discover all reflogs
+            for ref_bytes in iter_reflogs(logs_dir):
+                # Read the reflog entries for this ref
+                for entry in r.read_reflog(ref_bytes):
+                    yield (ref_bytes, entry)

+ 26 - 1
dulwich/reflog.py

@@ -97,7 +97,7 @@ def read_reflog(f: BinaryIO) -> Generator[Entry, None, None]:
     Returns: Iterator over Entry objects
     Returns: Iterator over Entry objects
     """
     """
     for line in f:
     for line in f:
-        yield parse_reflog_line(line)
+        yield parse_reflog_line(line.rstrip(b"\n"))
 
 
 
 
 def drop_reflog_entry(f: BinaryIO, index: int, rewrite: bool = False) -> None:
 def drop_reflog_entry(f: BinaryIO, index: int, rewrite: bool = False) -> None:
@@ -157,3 +157,28 @@ def drop_reflog_entry(f: BinaryIO, index: int, rewrite: bool = False) -> None:
             )
             )
         )
         )
     f.truncate()
     f.truncate()
+
+
+def iter_reflogs(logs_dir: str) -> Generator[bytes, None, None]:
+    """Iterate over all reflogs in a repository.
+
+    Args:
+        logs_dir: Path to the logs directory (e.g., .git/logs)
+
+    Yields:
+        Reference names (as bytes) that have reflogs
+    """
+    import os
+    from pathlib import Path
+
+    if not os.path.exists(logs_dir):
+        return
+
+    logs_path = Path(logs_dir)
+    for log_file in logs_path.rglob("*"):
+        if log_file.is_file():
+            # Get the ref name by removing the logs_dir prefix
+            ref_name = str(log_file.relative_to(logs_path))
+            # Convert path separators to / for refs
+            ref_name = ref_name.replace(os.sep, "/")
+            yield ref_name.encode("utf-8")

+ 18 - 0
dulwich/repo.py

@@ -1338,6 +1338,24 @@ class Repo(BaseRepo):
                 + b"\n"
                 + b"\n"
             )
             )
 
 
+    def read_reflog(self, ref):
+        """Read reflog entries for a reference.
+
+        Args:
+          ref: Reference name (e.g. b'HEAD', b'refs/heads/master')
+
+        Yields:
+          reflog.Entry objects in chronological order (oldest first)
+        """
+        from .reflog import read_reflog
+
+        path = os.path.join(self.controldir(), "logs", os.fsdecode(ref))
+        try:
+            with open(path, "rb") as f:
+                yield from read_reflog(f)
+        except FileNotFoundError:
+            return
+
     @classmethod
     @classmethod
     def discover(cls, start="."):
     def discover(cls, start="."):
         """Iterate parent directories to discover a repository.
         """Iterate parent directories to discover a repository.

+ 138 - 1
tests/test_porcelain.py

@@ -7419,4 +7419,141 @@ class BisectTests(PorcelainTestCase):
         # Skip c2 if it's selected
         # Skip c2 if it's selected
         next_sha = porcelain.bisect_skip(self.repo_path, [c2.id])
         next_sha = porcelain.bisect_skip(self.repo_path, [c2.id])
         self.assertIsNotNone(next_sha)
         self.assertIsNotNone(next_sha)
-        self.assertNotEqual(next_sha, c2.id)
+
+
+class ReflogTest(PorcelainTestCase):
+    def test_reflog_head(self):
+        """Test reading HEAD reflog."""
+        # Create a commit
+        blob = Blob.from_string(b"test content")
+        self.repo.object_store.add_object(blob)
+
+        tree = Tree()
+        tree.add(b"test", 0o100644, blob.id)
+        self.repo.object_store.add_object(tree)
+
+        commit = Commit()
+        commit.tree = tree.id
+        commit.author = b"Test Author <test@example.com>"
+        commit.committer = b"Test Author <test@example.com>"
+        commit.commit_time = 1234567890
+        commit.commit_timezone = 0
+        commit.author_time = 1234567890
+        commit.author_timezone = 0
+        commit.message = b"Initial commit"
+        self.repo.object_store.add_object(commit)
+
+        # Write a reflog entry
+        self.repo._write_reflog(
+            b"HEAD",
+            ZERO_SHA,
+            commit.id,
+            b"Test Author <test@example.com>",
+            1234567890,
+            0,
+            b"commit (initial): Initial commit",
+        )
+
+        # Read reflog using porcelain
+        entries = list(porcelain.reflog(self.repo_path))
+        self.assertEqual(1, len(entries))
+        self.assertEqual(ZERO_SHA, entries[0].old_sha)
+        self.assertEqual(commit.id, entries[0].new_sha)
+        self.assertEqual(b"Test Author <test@example.com>", entries[0].committer)
+        self.assertEqual(1234567890, entries[0].timestamp)
+        self.assertEqual(0, entries[0].timezone)
+        self.assertEqual(b"commit (initial): Initial commit", entries[0].message)
+
+    def test_reflog_with_string_ref(self):
+        """Test reading reflog with string reference."""
+        # Create a commit
+        blob = Blob.from_string(b"test content")
+        self.repo.object_store.add_object(blob)
+
+        tree = Tree()
+        tree.add(b"test", 0o100644, blob.id)
+        self.repo.object_store.add_object(tree)
+
+        commit = Commit()
+        commit.tree = tree.id
+        commit.author = b"Test Author <test@example.com>"
+        commit.committer = b"Test Author <test@example.com>"
+        commit.commit_time = 1234567890
+        commit.commit_timezone = 0
+        commit.author_time = 1234567890
+        commit.author_timezone = 0
+        commit.message = b"Initial commit"
+        self.repo.object_store.add_object(commit)
+
+        # Write a reflog entry
+        self.repo._write_reflog(
+            b"refs/heads/master",
+            ZERO_SHA,
+            commit.id,
+            b"Test Author <test@example.com>",
+            1234567890,
+            0,
+            b"commit (initial): Initial commit",
+        )
+
+        # Read reflog using porcelain with string ref
+        entries = list(porcelain.reflog(self.repo_path, "refs/heads/master"))
+        self.assertEqual(1, len(entries))
+        self.assertEqual(commit.id, entries[0].new_sha)
+
+    def test_reflog_all(self):
+        """Test reading all reflogs."""
+        # Create a commit
+        blob = Blob.from_string(b"test content")
+        self.repo.object_store.add_object(blob)
+
+        tree = Tree()
+        tree.add(b"test", 0o100644, blob.id)
+        self.repo.object_store.add_object(tree)
+
+        commit = Commit()
+        commit.tree = tree.id
+        commit.author = b"Test Author <test@example.com>"
+        commit.committer = b"Test Author <test@example.com>"
+        commit.commit_time = 1234567890
+        commit.commit_timezone = 0
+        commit.author_time = 1234567890
+        commit.author_timezone = 0
+        commit.message = b"Initial commit"
+        self.repo.object_store.add_object(commit)
+
+        # Write reflog entries for HEAD and master
+        self.repo._write_reflog(
+            b"HEAD",
+            ZERO_SHA,
+            commit.id,
+            b"Test Author <test@example.com>",
+            1234567890,
+            0,
+            b"commit (initial): Initial commit",
+        )
+        self.repo._write_reflog(
+            b"refs/heads/master",
+            ZERO_SHA,
+            commit.id,
+            b"Test Author <test@example.com>",
+            1234567891,
+            0,
+            b"branch: Created from HEAD",
+        )
+
+        # Read all reflogs using porcelain
+        entries = list(porcelain.reflog(self.repo_path, all=True))
+
+        # Should have at least 2 entries (HEAD and refs/heads/master)
+        self.assertGreaterEqual(len(entries), 2)
+
+        # Check that we got entries from different refs
+        refs_seen = set()
+        for ref, entry in entries:
+            refs_seen.add(ref)
+            self.assertEqual(commit.id, entry.new_sha)
+
+        # Should have seen at least HEAD and refs/heads/master
+        self.assertIn(b"HEAD", refs_seen)
+        self.assertIn(b"refs/heads/master", refs_seen)

+ 123 - 1
tests/test_reflog.py

@@ -21,15 +21,18 @@
 
 
 """Tests for dulwich.reflog."""
 """Tests for dulwich.reflog."""
 
 
+import tempfile
 from io import BytesIO
 from io import BytesIO
 
 
-from dulwich.objects import ZERO_SHA
+from dulwich.objects import ZERO_SHA, Blob, Commit, Tree
 from dulwich.reflog import (
 from dulwich.reflog import (
     drop_reflog_entry,
     drop_reflog_entry,
     format_reflog_line,
     format_reflog_line,
+    iter_reflogs,
     parse_reflog_line,
     parse_reflog_line,
     read_reflog,
     read_reflog,
 )
 )
+from dulwich.repo import Repo
 
 
 from . import TestCase
 from . import TestCase
 
 
@@ -142,3 +145,122 @@ class ReflogDropTests(TestCase):
         self.assertEqual(len(log), 1)
         self.assertEqual(len(log), 1)
         self.assertEqual(ZERO_SHA, log[0].old_sha)
         self.assertEqual(ZERO_SHA, log[0].old_sha)
         self.assertEqual(self.original_log[2].new_sha, log[0].new_sha)
         self.assertEqual(self.original_log[2].new_sha, log[0].new_sha)
+
+
+class RepoReflogTests(TestCase):
+    def setUp(self) -> None:
+        TestCase.setUp(self)
+        self.test_dir = tempfile.mkdtemp()
+        self.repo = Repo.init(self.test_dir)
+
+    def tearDown(self) -> None:
+        TestCase.tearDown(self)
+        import shutil
+
+        shutil.rmtree(self.test_dir)
+
+    def test_read_reflog_nonexistent(self) -> None:
+        # Reading a reflog that doesn't exist should return empty
+        entries = list(self.repo.read_reflog(b"refs/heads/nonexistent"))
+        self.assertEqual([], entries)
+
+    def test_read_reflog_head(self) -> None:
+        # Create a commit to generate a reflog entry
+        blob = Blob.from_string(b"test content")
+        self.repo.object_store.add_object(blob)
+
+        tree = Tree()
+        tree.add(b"test", 0o100644, blob.id)
+        self.repo.object_store.add_object(tree)
+
+        commit = Commit()
+        commit.tree = tree.id
+        commit.author = b"Test Author <test@example.com>"
+        commit.committer = b"Test Author <test@example.com>"
+        commit.commit_time = 1234567890
+        commit.commit_timezone = 0
+        commit.author_time = 1234567890
+        commit.author_timezone = 0
+        commit.message = b"Initial commit"
+        self.repo.object_store.add_object(commit)
+
+        # Manually write a reflog entry
+        self.repo._write_reflog(
+            b"HEAD",
+            ZERO_SHA,
+            commit.id,
+            b"Test Author <test@example.com>",
+            1234567890,
+            0,
+            b"commit (initial): Initial commit",
+        )
+
+        # Read the reflog
+        entries = list(self.repo.read_reflog(b"HEAD"))
+        self.assertEqual(1, len(entries))
+        self.assertEqual(ZERO_SHA, entries[0].old_sha)
+        self.assertEqual(commit.id, entries[0].new_sha)
+        self.assertEqual(b"Test Author <test@example.com>", entries[0].committer)
+        self.assertEqual(1234567890, entries[0].timestamp)
+        self.assertEqual(0, entries[0].timezone)
+        self.assertEqual(b"commit (initial): Initial commit", entries[0].message)
+
+    def test_iter_reflogs(self) -> None:
+        # Create commits and reflog entries
+        blob = Blob.from_string(b"test content")
+        self.repo.object_store.add_object(blob)
+
+        tree = Tree()
+        tree.add(b"test", 0o100644, blob.id)
+        self.repo.object_store.add_object(tree)
+
+        commit = Commit()
+        commit.tree = tree.id
+        commit.author = b"Test Author <test@example.com>"
+        commit.committer = b"Test Author <test@example.com>"
+        commit.commit_time = 1234567890
+        commit.commit_timezone = 0
+        commit.author_time = 1234567890
+        commit.author_timezone = 0
+        commit.message = b"Initial commit"
+        self.repo.object_store.add_object(commit)
+
+        # Write reflog entries for multiple refs
+        self.repo._write_reflog(
+            b"HEAD",
+            ZERO_SHA,
+            commit.id,
+            b"Test Author <test@example.com>",
+            1234567890,
+            0,
+            b"commit (initial): Initial commit",
+        )
+        self.repo._write_reflog(
+            b"refs/heads/master",
+            ZERO_SHA,
+            commit.id,
+            b"Test Author <test@example.com>",
+            1234567891,
+            0,
+            b"branch: Created from HEAD",
+        )
+        self.repo._write_reflog(
+            b"refs/heads/develop",
+            ZERO_SHA,
+            commit.id,
+            b"Test Author <test@example.com>",
+            1234567892,
+            0,
+            b"branch: Created from HEAD",
+        )
+
+        # Use iter_reflogs to get all reflogs
+        import os
+
+        logs_dir = os.path.join(self.repo.controldir(), "logs")
+        reflogs = list(iter_reflogs(logs_dir))
+
+        # Should have at least HEAD, refs/heads/master, and refs/heads/develop
+        self.assertIn(b"HEAD", reflogs)
+        self.assertIn(b"refs/heads/master", reflogs)
+        self.assertIn(b"refs/heads/develop", reflogs)