瀏覽代碼

Add porcelain.shortlog function to summarize commits by author (#1865)

This PR adds a new function porcelain.shortlog to the Dulwich porcelain
module. The function summarizes commits by author, similar to Git’s git
shortlog.
Muhammad Usama 4 月之前
父節點
當前提交
cd85cf6fa1
共有 4 個文件被更改,包括 129 次插入0 次删除
  1. 3 0
      NEWS
  2. 37 0
      dulwich/cli.py
  3. 43 0
      dulwich/porcelain.py
  4. 46 0
      tests/test_porcelain.py

+ 3 - 0
NEWS

@@ -1,5 +1,8 @@
 0.24.2	UNRELEASED
 
+ * Added ``porcelain.shortlog`` function to summarize commits by author,
+   similar to git shortlog. (Muhammad Usama, #1693)
+
  * Fix merge functionality to gracefully handle missing optional merge3 dependency
    by raising informative ImportError with installation instructions.
    (Jelmer Vernooij, #1759)

+ 37 - 0
dulwich/cli.py

@@ -1551,6 +1551,42 @@ class cmd_upload_pack(Command):
         porcelain.upload_pack(args.gitdir)
 
 
+class cmd_shortlog(Command):
+    """Show a shortlog of commits by author."""
+
+    def run(self, args) -> None:
+        """Execute the shortlog command with the given CLI arguments.
+
+        Args:
+            args: List of command line arguments.
+        """
+        parser = argparse.ArgumentParser()
+        parser.add_argument("gitdir", nargs="?", default=".", help="Git directory")
+        parser.add_argument("--summary", action="store_true", help="Show summary only")
+        parser.add_argument(
+            "--sort", action="store_true", help="Sort authors by commit count"
+        )
+        args = parser.parse_args(args)
+
+        shortlog_items: list[dict[str, str]] = porcelain.shortlog(
+            repo=args.gitdir,
+            summary_only=args.summary,
+            sort_by_commits=args.sort,
+        )
+
+        for item in shortlog_items:
+            author: str = item["author"]
+            messages: str = item["messages"]
+            if args.summary:
+                count = len(messages.splitlines())
+                sys.stdout.write(f"{count}\t{author}\n")
+            else:
+                sys.stdout.write(f"{author} ({len(messages.splitlines())}):\n")
+                for msg in messages.splitlines():
+                    sys.stdout.write(f"    {msg}\n")
+                sys.stdout.write("\n")
+
+
 class cmd_status(Command):
     """Show the working tree status."""
 
@@ -3960,6 +3996,7 @@ commands = {
     "show": cmd_show,
     "stash": cmd_stash,
     "status": cmd_status,
+    "shortlog": cmd_shortlog,
     "symbolic-ref": cmd_symbolic_ref,
     "submodule": cmd_submodule,
     "tag": cmd_tag,

+ 43 - 0
dulwich/porcelain.py

@@ -64,6 +64,7 @@ Currently implemented:
  * update_server_info
  * write_commit_graph
  * status
+ * shortlog
  * symbolic_ref
  * worktree{_add,_list,_remove,_prune,_lock,_unlock,_move}
 
@@ -2670,6 +2671,48 @@ def status(
         return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
 
 
+def shortlog(
+    repo: Union[str, os.PathLike, Repo],
+    summary_only: bool = False,
+    sort_by_commits: bool = False,
+) -> list[dict[str, str]]:
+    """Summarize commits by author, like git shortlog.
+
+    Args:
+        repo: Path to repository or Repo object.
+        summary_only: If True, only show counts per author.
+        sort_by_commits: If True, sort authors by number of commits.
+
+    Returns:
+        A list of dictionaries, each containing:
+            - "author": the author's name as a string
+            - "messages": all commit messages concatenated into a single string
+    """
+    with open_repo_closing(repo) as r:
+        walker = r.get_walker()
+        authors: dict[str, list[str]] = {}
+
+        for entry in walker:
+            commit = entry.commit
+
+            author = commit.author.decode(commit.encoding or "utf-8")
+            message = commit.message.decode(commit.encoding or "utf-8").strip()
+
+            authors.setdefault(author, []).append(message)
+
+        # Convert messages to single string per author
+        items: list[dict[str, str]] = [
+            {"author": author, "messages": "\n".join(msgs)}
+            for author, msgs in authors.items()
+        ]
+
+        if sort_by_commits:
+            # Sort by number of commits (lines in messages)
+            items.sort(key=lambda x: len(x["messages"].splitlines()), reverse=True)
+
+        return items
+
+
 def _walk_working_dir_paths(
     frompath: Union[str, bytes, os.PathLike],
     basepath: Union[str, bytes, os.PathLike],

+ 46 - 0
tests/test_porcelain.py

@@ -6343,6 +6343,52 @@ class StatusTests(PorcelainTestCase):
 # TODO(jelmer): Add test for dulwich.porcelain.daemon
 
 
+class ShortlogTests(PorcelainTestCase):
+    def test_shortlog(self) -> None:
+        """Test porcelain.shortlog function with multiple authors and commits."""
+
+        # Create first file and commit
+        file_a = os.path.join(self.repo.path, "a.txt")
+        with open(file_a, "w") as f:
+            f.write("hello")
+        porcelain.add(self.repo.path, paths=[file_a])
+        porcelain.commit(
+            repo=self.repo.path,
+            message=b"Initial commit",
+            author=b"John <john@example.com>",
+        )
+
+        # Create second file and commit
+        file_b = os.path.join(self.repo.path, "b.txt")
+        with open(file_b, "w") as f:
+            f.write("update")
+        porcelain.add(self.repo.path, paths=[file_b])
+        porcelain.commit(
+            repo=self.repo.path,
+            message=b"Update file",
+            author=b"Doe <doe@example.com>",
+        )
+
+        # Call shortlog
+        output = porcelain.shortlog(self.repo.path)
+        expected = [
+            {"author": "John <john@example.com>", "messages": "Initial commit"},
+            {"author": "Doe <doe@example.com>", "messages": "Update file"},
+        ]
+        self.assertCountEqual(output, expected)
+
+        # Test summary output (count of messages)
+        output_summary = [
+            {"author": entry["author"], "count": len(entry["messages"].splitlines())}
+            for entry in output
+        ]
+        expected_summary = [
+            {"author": "John <john@example.com>", "count": 1},
+            {"author": "Doe <doe@example.com>", "count": 1},
+        ]
+        self.assertCountEqual(output_summary, expected_summary)
+
+
 class UploadPackTests(PorcelainTestCase):
     """Tests for upload_pack."""