Jelmer Vernooij 5 месяцев назад
Родитель
Сommit
778a183937
53 измененных файлов с 3303 добавлено и 82 удалено
  1. 10 0
      dulwich/__init__.py
  2. 15 3
      dulwich/archive.py
  3. 5 0
      dulwich/attrs.py
  4. 5 0
      dulwich/bisect.py
  5. 10 0
      dulwich/bundle.py
  6. 422 0
      dulwich/cli.py
  7. 139 2
      dulwich/client.py
  8. 9 0
      dulwich/cloud/gcs.py
  9. 22 0
      dulwich/commit_graph.py
  10. 165 0
      dulwich/config.py
  11. 6 2
      dulwich/contrib/diffstat.py
  12. 55 0
      dulwich/contrib/paramiko_vendor.py
  13. 20 0
      dulwich/contrib/requests_vendor.py
  14. 126 4
      dulwich/contrib/swift.py
  15. 9 0
      dulwich/credentials.py
  16. 16 0
      dulwich/diff_tree.py
  17. 6 0
      dulwich/dumb.py
  18. 72 0
      dulwich/errors.py
  19. 74 0
      dulwich/fastexport.py
  20. 6 0
      dulwich/file.py
  21. 22 0
      dulwich/filters.py
  22. 40 0
      dulwich/graph.py
  23. 12 0
      dulwich/greenthreads.py
  24. 33 0
      dulwich/hooks.py
  25. 67 0
      dulwich/ignore.py
  26. 195 2
      dulwich/index.py
  27. 6 0
      dulwich/lfs.py
  28. 7 0
      dulwich/lfs_server.py
  29. 8 6
      dulwich/line_ending.py
  30. 18 0
      dulwich/lru_cache.py
  31. 21 0
      dulwich/mailmap.py
  32. 6 0
      dulwich/merge.py
  33. 47 0
      dulwich/notes.py
  34. 21 0
      dulwich/object_store.py
  35. 106 0
      dulwich/objects.py
  36. 15 0
      dulwich/objectspec.py
  37. 437 4
      dulwich/pack.py
  38. 48 2
      dulwich/patch.py
  39. 34 11
      dulwich/porcelain.py
  40. 144 0
      dulwich/protocol.py
  41. 10 0
      dulwich/rebase.py
  42. 236 0
      dulwich/refs.py
  43. 28 0
      dulwich/reftable.py
  44. 118 21
      dulwich/repo.py
  45. 181 0
      dulwich/server.py
  46. 1 2
      dulwich/sparse_patterns.py
  47. 13 0
      dulwich/stash.py
  48. 41 3
      dulwich/tests/test_object_store.py
  49. 1 0
      dulwich/tests/utils.py
  50. 8 0
      dulwich/walk.py
  51. 163 9
      dulwich/web.py
  52. 16 1
      dulwich/worktree.py
  53. 8 10
      pyproject.toml

+ 10 - 0
dulwich/__init__.py

@@ -48,6 +48,16 @@ except ImportError:
         since: Optional[Union[str, tuple[int, ...]]] = None,
         since: Optional[Union[str, tuple[int, ...]]] = None,
         remove_in: Optional[Union[str, tuple[int, ...]]] = None,
         remove_in: Optional[Union[str, tuple[int, ...]]] = None,
     ) -> Callable[[F], F]:
     ) -> Callable[[F], F]:
+        """Decorator to mark functions as deprecated.
+
+        Args:
+            since: Version when the function was deprecated
+            remove_in: Version when the function will be removed
+
+        Returns:
+            Decorator function
+        """
+
         def decorator(func: F) -> F:
         def decorator(func: F) -> F:
             import functools
             import functools
             import warnings
             import warnings

+ 15 - 3
dulwich/archive.py

@@ -51,10 +51,23 @@ class ChunkedBytesIO:
     """
     """
 
 
     def __init__(self, contents: list[bytes]) -> None:
     def __init__(self, contents: list[bytes]) -> None:
+        """Initialize ChunkedBytesIO.
+
+        Args:
+            contents: List of byte chunks
+        """
         self.contents = contents
         self.contents = contents
         self.pos = (0, 0)
         self.pos = (0, 0)
 
 
     def read(self, maxbytes: Optional[int] = None) -> bytes:
     def read(self, maxbytes: Optional[int] = None) -> bytes:
+        """Read bytes from the chunked stream.
+
+        Args:
+            maxbytes: Maximum number of bytes to read (None for all)
+
+        Returns:
+            Bytes read
+        """
         if maxbytes is None or maxbytes < 0:
         if maxbytes is None or maxbytes < 0:
             remaining = None
             remaining = None
         else:
         else:
@@ -98,6 +111,7 @@ def tar_stream(
       tree: Tree object for the tree root
       tree: Tree object for the tree root
       mtime: UNIX timestamp that is assigned as the modification time for
       mtime: UNIX timestamp that is assigned as the modification time for
         all files, and the gzip header modification time if format='gz'
         all files, and the gzip header modification time if format='gz'
+      prefix: Optional prefix to prepend to all paths in the archive
       format: Optional compression format for tarball
       format: Optional compression format for tarball
     Returns:
     Returns:
       Bytestrings
       Bytestrings
@@ -150,9 +164,7 @@ def tar_stream(
 def _walk_tree(
 def _walk_tree(
     store: "BaseObjectStore", tree: "Tree", root: bytes = b""
     store: "BaseObjectStore", tree: "Tree", root: bytes = b""
 ) -> Generator[tuple[bytes, "TreeEntry"], None, None]:
 ) -> Generator[tuple[bytes, "TreeEntry"], None, None]:
-    """Recursively walk a dulwich Tree, yielding tuples of
-    (absolute path, TreeEntry) along the way.
-    """
+    """Recursively walk a dulwich Tree, yielding tuples of (absolute path, TreeEntry) along the way."""
     for entry in tree.iteritems():
     for entry in tree.iteritems():
         entry_abspath = posixpath.join(root, entry.path)
         entry_abspath = posixpath.join(root, entry.path)
         if stat.S_ISDIR(entry.mode):
         if stat.S_ISDIR(entry.mode):

+ 5 - 0
dulwich/attrs.py

@@ -164,6 +164,11 @@ class Pattern:
     """A single gitattributes pattern."""
     """A single gitattributes pattern."""
 
 
     def __init__(self, pattern: bytes):
     def __init__(self, pattern: bytes):
+        """Initialize GitAttributesPattern.
+
+        Args:
+            pattern: Attribute pattern as bytes
+        """
         self.pattern = pattern
         self.pattern = pattern
         self._regex: Optional[re.Pattern[bytes]] = None
         self._regex: Optional[re.Pattern[bytes]] = None
         self._compile()
         self._compile()

+ 5 - 0
dulwich/bisect.py

@@ -32,6 +32,11 @@ class BisectState:
     """Manages the state of a bisect session."""
     """Manages the state of a bisect session."""
 
 
     def __init__(self, repo: Repo) -> None:
     def __init__(self, repo: Repo) -> None:
+        """Initialize BisectState.
+
+        Args:
+            repo: Repository to perform bisect on
+        """
         self.repo = repo
         self.repo = repo
         self._bisect_dir = os.path.join(repo.controldir(), "BISECT_START")
         self._bisect_dir = os.path.join(repo.controldir(), "BISECT_START")
 
 

+ 10 - 0
dulwich/bundle.py

@@ -32,6 +32,8 @@ if TYPE_CHECKING:
 
 
 
 
 class Bundle:
 class Bundle:
+    """Git bundle object representation."""
+
     version: Optional[int]
     version: Optional[int]
 
 
     capabilities: dict[str, Optional[str]]
     capabilities: dict[str, Optional[str]]
@@ -40,6 +42,7 @@ class Bundle:
     pack_data: PackData
     pack_data: PackData
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of Bundle."""
         return (
         return (
             f"<{type(self).__name__}(version={self.version}, "
             f"<{type(self).__name__}(version={self.version}, "
             f"capabilities={self.capabilities}, "
             f"capabilities={self.capabilities}, "
@@ -48,6 +51,7 @@ class Bundle:
         )
         )
 
 
     def __eq__(self, other: object) -> bool:
     def __eq__(self, other: object) -> bool:
+        """Check equality with another Bundle."""
         if not isinstance(other, type(self)):
         if not isinstance(other, type(self)):
             return False
             return False
         if self.version != other.version:
         if self.version != other.version:
@@ -154,6 +158,12 @@ def read_bundle(f: BinaryIO) -> Bundle:
 
 
 
 
 def write_bundle(f: BinaryIO, bundle: Bundle) -> None:
 def write_bundle(f: BinaryIO, bundle: Bundle) -> None:
+    """Write a bundle to a file.
+
+    Args:
+        f: File-like object to write to
+        bundle: Bundle object to write
+    """
     version = bundle.version
     version = bundle.version
     if version is None:
     if version is None:
         if bundle.capabilities:
         if bundle.capabilities:

Разница между файлами не показана из-за своего большого размера
+ 422 - 0
dulwich/cli.py


+ 139 - 2
dulwich/client.py

@@ -150,6 +150,11 @@ class InvalidWants(Exception):
     """Invalid wants."""
     """Invalid wants."""
 
 
     def __init__(self, wants) -> None:
     def __init__(self, wants) -> None:
+        """Initialize InvalidWants exception.
+
+        Args:
+            wants: List of invalid wants
+        """
         Exception.__init__(
         Exception.__init__(
             self, f"requested wants not in server provided refs: {wants!r}"
             self, f"requested wants not in server provided refs: {wants!r}"
         )
         )
@@ -159,6 +164,12 @@ class HTTPUnauthorized(Exception):
     """Raised when authentication fails."""
     """Raised when authentication fails."""
 
 
     def __init__(self, www_authenticate, url) -> None:
     def __init__(self, www_authenticate, url) -> None:
+        """Initialize HTTPUnauthorized exception.
+
+        Args:
+            www_authenticate: WWW-Authenticate header value
+            url: URL that requires authentication
+        """
         Exception.__init__(self, "No valid credentials provided")
         Exception.__init__(self, "No valid credentials provided")
         self.www_authenticate = www_authenticate
         self.www_authenticate = www_authenticate
         self.url = url
         self.url = url
@@ -168,6 +179,12 @@ class HTTPProxyUnauthorized(Exception):
     """Raised when proxy authentication fails."""
     """Raised when proxy authentication fails."""
 
 
     def __init__(self, proxy_authenticate, url) -> None:
     def __init__(self, proxy_authenticate, url) -> None:
+        """Initialize HTTPProxyUnauthorized exception.
+
+        Args:
+            proxy_authenticate: Proxy-Authenticate header value
+            url: URL that requires proxy authentication
+        """
         Exception.__init__(self, "No valid proxy credentials provided")
         Exception.__init__(self, "No valid proxy credentials provided")
         self.proxy_authenticate = proxy_authenticate
         self.proxy_authenticate = proxy_authenticate
         self.url = url
         self.url = url
@@ -211,6 +228,7 @@ class ReportStatusParser:
     """Handle status as reported by servers with 'report-status' capability."""
     """Handle status as reported by servers with 'report-status' capability."""
 
 
     def __init__(self) -> None:
     def __init__(self) -> None:
+        """Initialize ReportStatusParser."""
         self._done = False
         self._done = False
         self._pack_status = None
         self._pack_status = None
         self._ref_statuses: list[bytes] = []
         self._ref_statuses: list[bytes] = []
@@ -409,6 +427,15 @@ class FetchPackResult(_DeprecatedDictProxy):
     def __init__(
     def __init__(
         self, refs, symrefs, agent, new_shallow=None, new_unshallow=None
         self, refs, symrefs, agent, new_shallow=None, new_unshallow=None
     ) -> None:
     ) -> None:
+        """Initialize FetchPackResult.
+
+        Args:
+            refs: Dictionary with all remote refs
+            symrefs: Dictionary with remote symrefs
+            agent: User agent string
+            new_shallow: New shallow commits
+            new_unshallow: New unshallow commits
+        """
         self.refs = refs
         self.refs = refs
         self.symrefs = symrefs
         self.symrefs = symrefs
         self.agent = agent
         self.agent = agent
@@ -440,6 +467,12 @@ class LsRemoteResult(_DeprecatedDictProxy):
     """
     """
 
 
     def __init__(self, refs, symrefs) -> None:
     def __init__(self, refs, symrefs) -> None:
+        """Initialize LsRemoteResult.
+
+        Args:
+            refs: Dictionary with all remote refs
+            symrefs: Dictionary with remote symrefs
+        """
         self.refs = refs
         self.refs = refs
         self.symrefs = symrefs
         self.symrefs = symrefs
 
 
@@ -476,6 +509,13 @@ class SendPackResult(_DeprecatedDictProxy):
     """
     """
 
 
     def __init__(self, refs, agent=None, ref_status=None) -> None:
     def __init__(self, refs, agent=None, ref_status=None) -> None:
+        """Initialize SendPackResult.
+
+        Args:
+            refs: Dictionary with all remote refs
+            agent: User agent string
+            ref_status: Optional dictionary mapping ref name to error message
+        """
         self.refs = refs
         self.refs = refs
         self.agent = agent
         self.agent = agent
         self.ref_status = ref_status
         self.ref_status = ref_status
@@ -791,8 +831,10 @@ class GitClient:
           thin_packs: Whether or not thin packs should be retrieved
           thin_packs: Whether or not thin packs should be retrieved
           report_activity: Optional callback for reporting transport
           report_activity: Optional callback for reporting transport
             activity.
             activity.
+          quiet: Whether to suppress output
           include_tags: send annotated tags when sending the objects they point
           include_tags: send annotated tags when sending the objects they point
             to
             to
+          **kwargs: Additional keyword arguments
         """
         """
         self._report_activity = report_activity
         self._report_activity = report_activity
         self._report_status_parser: Optional[ReportStatusParser] = None
         self._report_status_parser: Optional[ReportStatusParser] = None
@@ -826,6 +868,7 @@ class GitClient:
 
 
         Args:
         Args:
           parsedurl: Result of urlparse()
           parsedurl: Result of urlparse()
+          **kwargs: Additional keyword arguments passed to the client constructor
 
 
         Returns:
         Returns:
           A `GitClient` object
           A `GitClient` object
@@ -1233,6 +1276,12 @@ class TraditionalGitClient(GitClient):
     DEFAULT_ENCODING = "utf-8"
     DEFAULT_ENCODING = "utf-8"
 
 
     def __init__(self, path_encoding=DEFAULT_ENCODING, **kwargs) -> None:
     def __init__(self, path_encoding=DEFAULT_ENCODING, **kwargs) -> None:
+        """Initialize a TraditionalGitClient.
+
+        Args:
+            path_encoding: Encoding for paths (default: utf-8)
+            **kwargs: Additional arguments passed to parent class
+        """
         self._remote_path_encoding = path_encoding
         self._remote_path_encoding = path_encoding
         super().__init__(**kwargs)
         super().__init__(**kwargs)
 
 
@@ -1559,6 +1608,18 @@ class TraditionalGitClient(GitClient):
         subdirs=None,
         subdirs=None,
         prefix=None,
         prefix=None,
     ) -> None:
     ) -> None:
+        """Request an archive of a specific commit.
+
+        Args:
+            path: Repository path
+            committish: Commit ID or ref to archive
+            write_data: Function to write archive data
+            progress: Optional progress callback
+            write_error: Optional error callback
+            format: Optional archive format
+            subdirs: Optional subdirectories to include
+            prefix: Optional prefix for archived files
+        """
         proto, can_read, stderr = self._connect(b"upload-archive", path)
         proto, can_read, stderr = self._connect(b"upload-archive", path)
         with proto:
         with proto:
             if format is not None:
             if format is not None:
@@ -1627,13 +1688,13 @@ class TCPGitClient(TraditionalGitClient):
         return cls(parsedurl.hostname, port=parsedurl.port, **kwargs)
         return cls(parsedurl.hostname, port=parsedurl.port, **kwargs)
 
 
     def get_url(self, path):
     def get_url(self, path):
-        """Get the URL for a TCP git connection.
+        r"""Get the URL for a TCP git connection.
 
 
         Args:
         Args:
           path: Repository path
           path: Repository path
 
 
         Returns:
         Returns:
-          git:// URL for the path
+          ``git://`` URL for the path
         """
         """
         netloc = self._host
         netloc = self._host
         if self._port is not None and self._port != TCP_GIT_PORT:
         if self._port is not None and self._port != TCP_GIT_PORT:
@@ -1847,6 +1908,7 @@ class LocalGitClient(GitClient):
           thin_packs: Whether or not thin packs should be retrieved
           thin_packs: Whether or not thin packs should be retrieved
           report_activity: Optional callback for reporting transport
           report_activity: Optional callback for reporting transport
             activity.
             activity.
+          config: Optional configuration object
         """
         """
         self._report_activity = report_activity
         self._report_activity = report_activity
         # Ignore the thin_packs argument
         # Ignore the thin_packs argument
@@ -1898,6 +1960,8 @@ class LocalGitClient(GitClient):
             Receive dict with existing remote refs, returns dict with
             Receive dict with existing remote refs, returns dict with
             changed refs (name -> sha, where sha=ZERO_SHA for deletions)
             changed refs (name -> sha, where sha=ZERO_SHA for deletions)
             with number of items and pack data to upload.
             with number of items and pack data to upload.
+          generate_pack_data: Function that generates pack data given
+            have and want object sets
           progress: Optional progress function
           progress: Optional progress function
 
 
         Returns:
         Returns:
@@ -1979,6 +2043,8 @@ class LocalGitClient(GitClient):
           filter_spec: A git-rev-list-style object filter spec, as bytestring.
           filter_spec: A git-rev-list-style object filter spec, as bytestring.
             Only used if the server supports the Git protocol-v2 'filter'
             Only used if the server supports the Git protocol-v2 'filter'
             feature, and ignored otherwise.
             feature, and ignored otherwise.
+          protocol_version: Optional Git protocol version
+          **kwargs: Additional keyword arguments
 
 
         Returns:
         Returns:
           FetchPackResult object
           FetchPackResult object
@@ -2022,6 +2088,7 @@ class LocalGitClient(GitClient):
           filter_spec: A git-rev-list-style object filter spec, as bytestring.
           filter_spec: A git-rev-list-style object filter spec, as bytestring.
             Only used if the server supports the Git protocol-v2 'filter'
             Only used if the server supports the Git protocol-v2 'filter'
             feature, and ignored otherwise.
             feature, and ignored otherwise.
+          protocol_version: Optional Git protocol version
 
 
         Returns:
         Returns:
           FetchPackResult object
           FetchPackResult object
@@ -2084,6 +2151,7 @@ class BundleClient(GitClient):
           thin_packs: Whether or not thin packs should be retrieved
           thin_packs: Whether or not thin packs should be retrieved
           report_activity: Optional callback for reporting transport
           report_activity: Optional callback for reporting transport
             activity.
             activity.
+          config: Optional configuration object
         """
         """
         self._report_activity = report_activity
         self._report_activity = report_activity
 
 
@@ -2383,6 +2451,11 @@ class StrangeHostname(Exception):
     """Refusing to connect to strange SSH hostname."""
     """Refusing to connect to strange SSH hostname."""
 
 
     def __init__(self, hostname) -> None:
     def __init__(self, hostname) -> None:
+        """Initialize StrangeHostname exception.
+
+        Args:
+            hostname: The strange hostname that was rejected
+        """
         super().__init__(hostname)
         super().__init__(hostname)
 
 
 
 
@@ -2400,6 +2473,21 @@ class SubprocessSSHVendor(SSHVendor):
         ssh_command=None,
         ssh_command=None,
         protocol_version: Optional[int] = None,
         protocol_version: Optional[int] = None,
     ):
     ):
+        """Run a git command over SSH.
+
+        Args:
+            host: SSH host to connect to
+            command: Git command to run
+            username: Optional username
+            port: Optional port number
+            password: Optional password (not supported)
+            key_filename: Optional SSH key file
+            ssh_command: Optional custom SSH command
+            protocol_version: Optional Git protocol version
+
+        Returns:
+            Tuple of (subprocess.Popen, Protocol, stderr_stream)
+        """
         if password is not None:
         if password is not None:
             raise NotImplementedError(
             raise NotImplementedError(
                 "Setting password not supported by SubprocessSSHVendor."
                 "Setting password not supported by SubprocessSSHVendor."
@@ -2453,6 +2541,21 @@ class PLinkSSHVendor(SSHVendor):
         ssh_command=None,
         ssh_command=None,
         protocol_version: Optional[int] = None,
         protocol_version: Optional[int] = None,
     ):
     ):
+        """Run a git command over SSH using PLink.
+
+        Args:
+            host: SSH host to connect to
+            command: Git command to run
+            username: Optional username
+            port: Optional port number
+            password: Optional password
+            key_filename: Optional SSH key file
+            ssh_command: Optional custom SSH command
+            protocol_version: Optional Git protocol version
+
+        Returns:
+            Tuple of (subprocess.Popen, Protocol, stderr_stream)
+        """
         if ssh_command:
         if ssh_command:
             import shlex
             import shlex
 
 
@@ -2506,6 +2609,7 @@ class PLinkSSHVendor(SSHVendor):
 
 
 
 
 def ParamikoSSHVendor(**kwargs):
 def ParamikoSSHVendor(**kwargs):
+    """Create a ParamikoSSHVendor (deprecated)."""
     import warnings
     import warnings
 
 
     warnings.warn(
     warnings.warn(
@@ -2522,6 +2626,8 @@ get_ssh_vendor: Callable[[], SSHVendor] = SubprocessSSHVendor
 
 
 
 
 class SSHGitClient(TraditionalGitClient):
 class SSHGitClient(TraditionalGitClient):
+    """Git client that connects over SSH."""
+
     def __init__(
     def __init__(
         self,
         self,
         host,
         host,
@@ -2534,6 +2640,19 @@ class SSHGitClient(TraditionalGitClient):
         ssh_command=None,
         ssh_command=None,
         **kwargs,
         **kwargs,
     ) -> None:
     ) -> None:
+        """Initialize SSHGitClient.
+
+        Args:
+            host: SSH hostname
+            port: Optional SSH port
+            username: Optional username
+            vendor: Optional SSH vendor
+            config: Optional configuration
+            password: Optional password
+            key_filename: Optional SSH key file
+            ssh_command: Optional custom SSH command
+            **kwargs: Additional keyword arguments
+        """
         self.host = host
         self.host = host
         self.port = port
         self.port = port
         self.username = username
         self.username = username
@@ -2566,6 +2685,7 @@ class SSHGitClient(TraditionalGitClient):
             self.ssh_vendor = get_ssh_vendor()
             self.ssh_vendor = get_ssh_vendor()
 
 
     def get_url(self, path):
     def get_url(self, path):
+        """Get the SSH URL for a path."""
         netloc = self.host
         netloc = self.host
         if self.port is not None:
         if self.port is not None:
             netloc += f":{self.port}"
             netloc += f":{self.port}"
@@ -2577,6 +2697,7 @@ class SSHGitClient(TraditionalGitClient):
 
 
     @classmethod
     @classmethod
     def from_parsedurl(cls, parsedurl, **kwargs):
     def from_parsedurl(cls, parsedurl, **kwargs):
+        """Create an SSHGitClient from a parsed URL."""
         return cls(
         return cls(
             host=parsedurl.hostname,
             host=parsedurl.hostname,
             port=parsedurl.port,
             port=parsedurl.port,
@@ -2636,6 +2757,7 @@ class SSHGitClient(TraditionalGitClient):
 
 
 
 
 def default_user_agent_string():
 def default_user_agent_string():
+    """Return the default user agent string for Dulwich."""
     # Start user agent with "git/", because GitHub requires this. :-( See
     # Start user agent with "git/", because GitHub requires this. :-( See
     # https://github.com/jelmer/dulwich/issues/562 for details.
     # https://github.com/jelmer/dulwich/issues/562 for details.
     return "git/dulwich/{}".format(".".join([str(x) for x in dulwich.__version__]))
     return "git/dulwich/{}".format(".".join([str(x) for x in dulwich.__version__]))
@@ -2655,6 +2777,9 @@ def default_urllib3_manager(
 
 
     Args:
     Args:
       config: `dulwich.config.ConfigDict` instance with Git configuration.
       config: `dulwich.config.ConfigDict` instance with Git configuration.
+      pool_manager_cls: Pool manager class to use
+      proxy_manager_cls: Proxy manager class to use
+      base_url: Base URL for proxy bypass checks
       timeout: Timeout for HTTP requests in seconds
       timeout: Timeout for HTTP requests in seconds
       override_kwargs: Additional arguments for `urllib3.ProxyManager`
       override_kwargs: Additional arguments for `urllib3.ProxyManager`
 
 
@@ -2756,6 +2881,7 @@ def default_urllib3_manager(
 
 
 
 
 def check_for_proxy_bypass(base_url) -> bool:
 def check_for_proxy_bypass(base_url) -> bool:
+    """Check if proxy should be bypassed for the given URL."""
     # Check if a proxy bypass is defined with the no_proxy environment variable
     # Check if a proxy bypass is defined with the no_proxy environment variable
     if base_url:  # only check if base_url is provided
     if base_url:  # only check if base_url is provided
         no_proxy_str = os.environ.get("no_proxy")
         no_proxy_str = os.environ.get("no_proxy")
@@ -2819,6 +2945,7 @@ class AbstractHttpGitClient(GitClient):
     """
     """
 
 
     def __init__(self, base_url, dumb=False, **kwargs) -> None:
     def __init__(self, base_url, dumb=False, **kwargs) -> None:
+        """Initialize AbstractHttpGitClient."""
         self._base_url = base_url.rstrip("/") + "/"
         self._base_url = base_url.rstrip("/") + "/"
         self.dumb = dumb
         self.dumb = dumb
         GitClient.__init__(self, **kwargs)
         GitClient.__init__(self, **kwargs)
@@ -2830,6 +2957,7 @@ class AbstractHttpGitClient(GitClient):
           url: Request URL.
           url: Request URL.
           headers: Optional custom headers to override defaults.
           headers: Optional custom headers to override defaults.
           data: Request data.
           data: Request data.
+          raise_for_status: Whether to raise an exception for HTTP errors.
 
 
         Returns:
         Returns:
           Tuple (response, read), where response is an urllib3
           Tuple (response, read), where response is an urllib3
@@ -3230,6 +3358,7 @@ class AbstractHttpGitClient(GitClient):
         return LsRemoteResult(refs, symrefs)
         return LsRemoteResult(refs, symrefs)
 
 
     def get_url(self, path):
     def get_url(self, path):
+        """Get the HTTP URL for a path."""
         return self._get_url(path).rstrip("/")
         return self._get_url(path).rstrip("/")
 
 
     def _get_url(self, path):
     def _get_url(self, path):
@@ -3237,6 +3366,7 @@ class AbstractHttpGitClient(GitClient):
 
 
     @classmethod
     @classmethod
     def from_parsedurl(cls, parsedurl, **kwargs):
     def from_parsedurl(cls, parsedurl, **kwargs):
+        """Create an AbstractHttpGitClient from a parsed URL."""
         password = parsedurl.password
         password = parsedurl.password
         if password is not None:
         if password is not None:
             kwargs["password"] = urlunquote(password)
             kwargs["password"] = urlunquote(password)
@@ -3246,6 +3376,7 @@ class AbstractHttpGitClient(GitClient):
         return cls(urlunparse(parsedurl), **kwargs)
         return cls(urlunparse(parsedurl), **kwargs)
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of this client."""
         return f"{type(self).__name__}({self._base_url!r}, dumb={self.dumb!r})"
         return f"{type(self).__name__}({self._base_url!r}, dumb={self.dumb!r})"
 
 
 
 
@@ -3262,6 +3393,8 @@ def _wrap_urllib3_exceptions(func):
 
 
 
 
 class Urllib3HttpGitClient(AbstractHttpGitClient):
 class Urllib3HttpGitClient(AbstractHttpGitClient):
+    """Git client that uses urllib3 for HTTP(S) connections."""
+
     def __init__(
     def __init__(
         self,
         self,
         base_url,
         base_url,
@@ -3273,6 +3406,7 @@ class Urllib3HttpGitClient(AbstractHttpGitClient):
         timeout=None,
         timeout=None,
         **kwargs,
         **kwargs,
     ) -> None:
     ) -> None:
+        """Initialize Urllib3HttpGitClient."""
         self._username = username
         self._username = username
         self._password = password
         self._password = password
         self._timeout = timeout
         self._timeout = timeout
@@ -3389,6 +3523,7 @@ def get_transport_and_path_from_url(
       url: URL to open (a unicode string)
       url: URL to open (a unicode string)
       config: Optional config object
       config: Optional config object
       operation: Kind of operation that'll be performed; "pull" or "push"
       operation: Kind of operation that'll be performed; "pull" or "push"
+      **kwargs: Additional keyword arguments
 
 
     Keyword Args:
     Keyword Args:
       thin_packs: Whether or not thin packs should be retrieved
       thin_packs: Whether or not thin packs should be retrieved
@@ -3460,6 +3595,7 @@ def get_transport_and_path(
       location: URL or path (a string)
       location: URL or path (a string)
       config: Optional config object
       config: Optional config object
       operation: Kind of operation that'll be performed; "pull" or "push"
       operation: Kind of operation that'll be performed; "pull" or "push"
+      **kwargs: Additional keyword arguments
 
 
     Keyword Args:
     Keyword Args:
       thin_packs: Whether or not thin packs should be retrieved
       thin_packs: Whether or not thin packs should be retrieved
@@ -3508,6 +3644,7 @@ DEFAULT_GIT_CREDENTIALS_PATHS = [
 def get_credentials_from_store(
 def get_credentials_from_store(
     scheme, hostname, username=None, fnames=DEFAULT_GIT_CREDENTIALS_PATHS
     scheme, hostname, username=None, fnames=DEFAULT_GIT_CREDENTIALS_PATHS
 ):
 ):
+    """Read credentials from a Git credential store."""
     for fname in fnames:
     for fname in fnames:
         try:
         try:
             with open(fname, "rb") as f:
             with open(fname, "rb") as f:

+ 9 - 0
dulwich/cloud/gcs.py

@@ -32,12 +32,21 @@ from ..pack import PACK_SPOOL_FILE_MAX_SIZE, Pack, PackData, load_pack_index_fil
 
 
 
 
 class GcsObjectStore(BucketBasedObjectStore):
 class GcsObjectStore(BucketBasedObjectStore):
+    """Object store implementation for Google Cloud Storage."""
+
     def __init__(self, bucket, subpath="") -> None:
     def __init__(self, bucket, subpath="") -> None:
+        """Initialize GcsObjectStore.
+
+        Args:
+          bucket: GCS bucket instance
+          subpath: Subpath within the bucket
+        """
         super().__init__()
         super().__init__()
         self.bucket = bucket
         self.bucket = bucket
         self.subpath = subpath
         self.subpath = subpath
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of GcsObjectStore."""
         return f"{type(self).__name__}({self.bucket!r}, subpath={self.subpath!r})"
         return f"{type(self).__name__}({self.bucket!r}, subpath={self.subpath!r})"
 
 
     def _remove_pack(self, name) -> None:
     def _remove_pack(self, name) -> None:

+ 22 - 0
dulwich/commit_graph.py

@@ -68,6 +68,15 @@ class CommitGraphEntry:
         generation: int,
         generation: int,
         commit_time: int,
         commit_time: int,
     ) -> None:
     ) -> None:
+        """Initialize CommitGraphEntry.
+
+        Args:
+          commit_id: The commit object ID
+          tree_id: The tree object ID
+          parents: List of parent commit IDs
+          generation: Generation number
+          commit_time: Commit timestamp
+        """
         self.commit_id = commit_id
         self.commit_id = commit_id
         self.tree_id = tree_id
         self.tree_id = tree_id
         self.parents = parents
         self.parents = parents
@@ -75,6 +84,7 @@ class CommitGraphEntry:
         self.commit_time = commit_time
         self.commit_time = commit_time
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of CommitGraphEntry."""
         return (
         return (
             f"CommitGraphEntry(commit_id={self.commit_id!r}, "
             f"CommitGraphEntry(commit_id={self.commit_id!r}, "
             f"tree_id={self.tree_id!r}, parents={self.parents!r}, "
             f"tree_id={self.tree_id!r}, parents={self.parents!r}, "
@@ -86,10 +96,17 @@ class CommitGraphChunk:
     """Represents a chunk in the commit graph file."""
     """Represents a chunk in the commit graph file."""
 
 
     def __init__(self, chunk_id: bytes, data: bytes) -> None:
     def __init__(self, chunk_id: bytes, data: bytes) -> None:
+        """Initialize CommitGraphChunk.
+
+        Args:
+          chunk_id: Chunk identifier
+          data: Chunk data
+        """
         self.chunk_id = chunk_id
         self.chunk_id = chunk_id
         self.data = data
         self.data = data
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of CommitGraphChunk."""
         return f"CommitGraphChunk(chunk_id={self.chunk_id!r}, size={len(self.data)})"
         return f"CommitGraphChunk(chunk_id={self.chunk_id!r}, size={len(self.data)})"
 
 
 
 
@@ -97,6 +114,11 @@ class CommitGraph:
     """Git commit graph file reader/writer."""
     """Git commit graph file reader/writer."""
 
 
     def __init__(self, hash_version: int = HASH_VERSION_SHA1) -> None:
     def __init__(self, hash_version: int = HASH_VERSION_SHA1) -> None:
+        """Initialize CommitGraph.
+
+        Args:
+          hash_version: Hash version to use (SHA1 or SHA256)
+        """
         self.hash_version = hash_version
         self.hash_version = hash_version
         self.chunks: dict[bytes, CommitGraphChunk] = {}
         self.chunks: dict[bytes, CommitGraphChunk] = {}
         self.entries: list[CommitGraphEntry] = []
         self.entries: list[CommitGraphEntry] = []

+ 165 - 0
dulwich/config.py

@@ -149,6 +149,17 @@ def match_glob_pattern(value: str, pattern: str) -> bool:
 
 
 
 
 def lower_key(key: ConfigKey) -> ConfigKey:
 def lower_key(key: ConfigKey) -> ConfigKey:
+    """Convert a config key to lowercase, preserving subsection case.
+
+    Args:
+      key: Configuration key (str, bytes, or tuple)
+
+    Returns:
+      Key with section names lowercased, subsection names preserved
+
+    Raises:
+      TypeError: If key is not str, bytes, or tuple
+    """
     if isinstance(key, (bytes, str)):
     if isinstance(key, (bytes, str)):
         return key.lower()
         return key.lower()
 
 
@@ -170,7 +181,18 @@ _T = TypeVar("_T")  # For get() default parameter
 
 
 
 
 class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
 class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
+    """A case-insensitive ordered dictionary that can store multiple values per key.
+
+    This class maintains the order of insertions and allows multiple values
+    for the same key. Keys are compared case-insensitively.
+    """
+
     def __init__(self, default_factory: Optional[Callable[[], V]] = None) -> None:
     def __init__(self, default_factory: Optional[Callable[[], V]] = None) -> None:
+        """Initialize a CaseInsensitiveOrderedMultiDict.
+
+        Args:
+          default_factory: Optional factory function for default values
+        """
         self._real: list[tuple[K, V]] = []
         self._real: list[tuple[K, V]] = []
         self._keyed: dict[Any, V] = {}
         self._keyed: dict[Any, V] = {}
         self._default_factory = default_factory
         self._default_factory = default_factory
@@ -183,6 +205,18 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
         ] = None,
         ] = None,
         default_factory: Optional[Callable[[], V]] = None,
         default_factory: Optional[Callable[[], V]] = None,
     ) -> "CaseInsensitiveOrderedMultiDict[K, V]":
     ) -> "CaseInsensitiveOrderedMultiDict[K, V]":
+        """Create a CaseInsensitiveOrderedMultiDict from an existing mapping.
+
+        Args:
+          dict_in: Optional mapping to initialize from
+          default_factory: Optional factory function for default values
+
+        Returns:
+          New CaseInsensitiveOrderedMultiDict instance
+
+        Raises:
+          TypeError: If dict_in is not a mapping or None
+        """
         if isinstance(dict_in, cls):
         if isinstance(dict_in, cls):
             return dict_in
             return dict_in
 
 
@@ -200,14 +234,20 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
         return out
         return out
 
 
     def __len__(self) -> int:
     def __len__(self) -> int:
+        """Return the number of unique keys in the dictionary."""
         return len(self._keyed)
         return len(self._keyed)
 
 
     def keys(self) -> KeysView[K]:
     def keys(self) -> KeysView[K]:
+        """Return a view of the dictionary's keys."""
         return self._keyed.keys()  # type: ignore[return-value]
         return self._keyed.keys()  # type: ignore[return-value]
 
 
     def items(self) -> ItemsView[K, V]:
     def items(self) -> ItemsView[K, V]:
+        """Return a view of the dictionary's (key, value) pairs in insertion order."""
+
         # Return a view that iterates over the real list to preserve order
         # Return a view that iterates over the real list to preserve order
         class OrderedItemsView(ItemsView[K, V]):
         class OrderedItemsView(ItemsView[K, V]):
+            """Items view that preserves insertion order."""
+
             def __init__(self, mapping: CaseInsensitiveOrderedMultiDict[K, V]):
             def __init__(self, mapping: CaseInsensitiveOrderedMultiDict[K, V]):
                 self._mapping = mapping
                 self._mapping = mapping
 
 
@@ -226,16 +266,25 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
         return OrderedItemsView(self)
         return OrderedItemsView(self)
 
 
     def __iter__(self) -> Iterator[K]:
     def __iter__(self) -> Iterator[K]:
+        """Iterate over the dictionary's keys."""
         return iter(self._keyed)
         return iter(self._keyed)
 
 
     def values(self) -> ValuesView[V]:
     def values(self) -> ValuesView[V]:
+        """Return a view of the dictionary's values."""
         return self._keyed.values()
         return self._keyed.values()
 
 
     def __setitem__(self, key: K, value: V) -> None:
     def __setitem__(self, key: K, value: V) -> None:
+        """Set a value for a key, appending to existing values."""
         self._real.append((key, value))
         self._real.append((key, value))
         self._keyed[lower_key(key)] = value
         self._keyed[lower_key(key)] = value
 
 
     def set(self, key: K, value: V) -> None:
     def set(self, key: K, value: V) -> None:
+        """Set a value for a key, replacing all existing values.
+
+        Args:
+          key: The key to set
+          value: The value to set
+        """
         # This method replaces all existing values for the key
         # This method replaces all existing values for the key
         lower = lower_key(key)
         lower = lower_key(key)
         self._real = [(k, v) for k, v in self._real if lower_key(k) != lower]
         self._real = [(k, v) for k, v in self._real if lower_key(k) != lower]
@@ -243,6 +292,11 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
         self._keyed[lower] = value
         self._keyed[lower] = value
 
 
     def __delitem__(self, key: K) -> None:
     def __delitem__(self, key: K) -> None:
+        """Delete all values for a key.
+
+        Raises:
+          KeyError: If the key is not found
+        """
         lower_k = lower_key(key)
         lower_k = lower_key(key)
         del self._keyed[lower_k]
         del self._keyed[lower_k]
         for i, (actual, unused_value) in reversed(list(enumerate(self._real))):
         for i, (actual, unused_value) in reversed(list(enumerate(self._real))):
@@ -250,9 +304,23 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
                 del self._real[i]
                 del self._real[i]
 
 
     def __getitem__(self, item: K) -> V:
     def __getitem__(self, item: K) -> V:
+        """Get the last value for a key.
+
+        Raises:
+          KeyError: If the key is not found
+        """
         return self._keyed[lower_key(item)]
         return self._keyed[lower_key(item)]
 
 
     def get(self, key: K, /, default: Union[V, _T, None] = None) -> Union[V, _T, None]:  # type: ignore[override]
     def get(self, key: K, /, default: Union[V, _T, None] = None) -> Union[V, _T, None]:  # type: ignore[override]
+        """Get the last value for a key, or a default if not found.
+
+        Args:
+          key: The key to look up
+          default: Default value to return if key not found
+
+        Returns:
+          The value for the key, or default/default_factory result if not found
+        """
         try:
         try:
             return self[key]
             return self[key]
         except KeyError:
         except KeyError:
@@ -264,12 +332,32 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
                 return None
                 return None
 
 
     def get_all(self, key: K) -> Iterator[V]:
     def get_all(self, key: K) -> Iterator[V]:
+        """Get all values for a key in insertion order.
+
+        Args:
+          key: The key to look up
+
+        Returns:
+          Iterator of all values for the key
+        """
         lowered_key = lower_key(key)
         lowered_key = lower_key(key)
         for actual, value in self._real:
         for actual, value in self._real:
             if lower_key(actual) == lowered_key:
             if lower_key(actual) == lowered_key:
                 yield value
                 yield value
 
 
     def setdefault(self, key: K, default: Optional[V] = None) -> V:
     def setdefault(self, key: K, default: Optional[V] = None) -> V:
+        """Get value for key, setting it to default if not present.
+
+        Args:
+          key: The key to look up
+          default: Default value to set if key not found
+
+        Returns:
+          The existing value or the newly set default
+
+        Raises:
+          KeyError: If key not found and no default or default_factory
+        """
         try:
         try:
             return self[key]
             return self[key]
         except KeyError:
         except KeyError:
@@ -338,6 +426,7 @@ class Config:
           section: Tuple with section name and optional subsection name
           section: Tuple with section name and optional subsection name
           name: Name of the setting, including section and possible
           name: Name of the setting, including section and possible
             subsection.
             subsection.
+          default: Default value if setting is not found
 
 
         Returns:
         Returns:
           Contents of the setting
           Contents of the setting
@@ -414,29 +503,45 @@ class ConfigDict(Config):
         )
         )
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of ConfigDict."""
         return f"{self.__class__.__name__}({self._values!r})"
         return f"{self.__class__.__name__}({self._values!r})"
 
 
     def __eq__(self, other: object) -> bool:
     def __eq__(self, other: object) -> bool:
+        """Check equality with another ConfigDict."""
         return isinstance(other, self.__class__) and other._values == self._values
         return isinstance(other, self.__class__) and other._values == self._values
 
 
     def __getitem__(self, key: Section) -> CaseInsensitiveOrderedMultiDict[Name, Value]:
     def __getitem__(self, key: Section) -> CaseInsensitiveOrderedMultiDict[Name, Value]:
+        """Get configuration values for a section.
+
+        Raises:
+          KeyError: If section not found
+        """
         return self._values.__getitem__(key)
         return self._values.__getitem__(key)
 
 
     def __setitem__(
     def __setitem__(
         self, key: Section, value: CaseInsensitiveOrderedMultiDict[Name, Value]
         self, key: Section, value: CaseInsensitiveOrderedMultiDict[Name, Value]
     ) -> None:
     ) -> None:
+        """Set configuration values for a section."""
         return self._values.__setitem__(key, value)
         return self._values.__setitem__(key, value)
 
 
     def __delitem__(self, key: Section) -> None:
     def __delitem__(self, key: Section) -> None:
+        """Delete a configuration section.
+
+        Raises:
+          KeyError: If section not found
+        """
         return self._values.__delitem__(key)
         return self._values.__delitem__(key)
 
 
     def __iter__(self) -> Iterator[Section]:
     def __iter__(self) -> Iterator[Section]:
+        """Iterate over configuration sections."""
         return self._values.__iter__()
         return self._values.__iter__()
 
 
     def __len__(self) -> int:
     def __len__(self) -> int:
+        """Return the number of sections."""
         return self._values.__len__()
         return self._values.__len__()
 
 
     def keys(self) -> KeysView[Section]:
     def keys(self) -> KeysView[Section]:
+        """Return a view of section names."""
         return self._values.keys()
         return self._values.keys()
 
 
     @classmethod
     @classmethod
@@ -468,6 +573,15 @@ class ConfigDict(Config):
         return checked_section, name
         return checked_section, name
 
 
     def get_multivar(self, section: SectionLike, name: NameLike) -> Iterator[Value]:
     def get_multivar(self, section: SectionLike, name: NameLike) -> Iterator[Value]:
+        """Get multiple values for a configuration setting.
+
+        Args:
+            section: Section name
+            name: Setting name
+
+        Returns:
+            Iterator of configuration values
+        """
         section, name = self._check_section_and_name(section, name)
         section, name = self._check_section_and_name(section, name)
 
 
         if len(section) > 1:
         if len(section) > 1:
@@ -483,6 +597,18 @@ class ConfigDict(Config):
         section: SectionLike,
         section: SectionLike,
         name: NameLike,
         name: NameLike,
     ) -> Value:
     ) -> Value:
+        """Get a configuration value.
+
+        Args:
+            section: Section name
+            name: Setting name
+
+        Returns:
+            Configuration value
+
+        Raises:
+            KeyError: if the value is not set
+        """
         section, name = self._check_section_and_name(section, name)
         section, name = self._check_section_and_name(section, name)
 
 
         if len(section) > 1:
         if len(section) > 1:
@@ -499,6 +625,13 @@ class ConfigDict(Config):
         name: NameLike,
         name: NameLike,
         value: Union[ValueLike, bool],
         value: Union[ValueLike, bool],
     ) -> None:
     ) -> None:
+        """Set a configuration value.
+
+        Args:
+            section: Section name
+            name: Setting name
+            value: Configuration value
+        """
         section, name = self._check_section_and_name(section, name)
         section, name = self._check_section_and_name(section, name)
 
 
         if isinstance(value, bool):
         if isinstance(value, bool):
@@ -531,6 +664,7 @@ class ConfigDict(Config):
         self._values.setdefault(section)[name] = value
         self._values.setdefault(section)[name] = value
 
 
     def items(self, section: SectionLike) -> Iterator[tuple[Name, Value]]:
     def items(self, section: SectionLike) -> Iterator[tuple[Name, Value]]:
+        """Get items in a section."""
         section_bytes, _ = self._check_section_and_name(section, b"")
         section_bytes, _ = self._check_section_and_name(section, b"")
         section_dict = self._values.get(section_bytes)
         section_dict = self._values.get(section_bytes)
         if section_dict is not None:
         if section_dict is not None:
@@ -538,6 +672,7 @@ class ConfigDict(Config):
         return iter([])
         return iter([])
 
 
     def sections(self) -> Iterator[Section]:
     def sections(self) -> Iterator[Section]:
+        """Get all sections."""
         return iter(self._values.keys())
         return iter(self._values.keys())
 
 
 
 
@@ -750,6 +885,12 @@ class ConfigFile(ConfigDict):
         ] = None,
         ] = None,
         encoding: Union[str, None] = None,
         encoding: Union[str, None] = None,
     ) -> None:
     ) -> None:
+        """Initialize a ConfigFile.
+
+        Args:
+          values: Optional mapping of configuration values
+          encoding: Optional encoding for the file (defaults to system encoding)
+        """
         super().__init__(values=values, encoding=encoding)
         super().__init__(values=values, encoding=encoding)
         self.path: Optional[str] = None
         self.path: Optional[str] = None
         self._included_paths: set[str] = set()  # Track included files to prevent cycles
         self._included_paths: set[str] = set()  # Track included files to prevent cycles
@@ -1140,6 +1281,14 @@ class ConfigFile(ConfigDict):
 
 
 
 
 def get_xdg_config_home_path(*path_segments: str) -> str:
 def get_xdg_config_home_path(*path_segments: str) -> str:
+    """Get a path in the XDG config home directory.
+
+    Args:
+      *path_segments: Path segments to join to the XDG config home
+
+    Returns:
+      Full path in XDG config home directory
+    """
     xdg_config_home = os.environ.get(
     xdg_config_home = os.environ.get(
         "XDG_CONFIG_HOME",
         "XDG_CONFIG_HOME",
         os.path.expanduser("~/.config/"),
         os.path.expanduser("~/.config/"),
@@ -1227,14 +1376,26 @@ class StackedConfig(Config):
     def __init__(
     def __init__(
         self, backends: list[ConfigFile], writable: Optional[ConfigFile] = None
         self, backends: list[ConfigFile], writable: Optional[ConfigFile] = None
     ) -> None:
     ) -> None:
+        """Initialize a StackedConfig.
+
+        Args:
+          backends: List of config files to read from (in order of precedence)
+          writable: Optional config file to write changes to
+        """
         self.backends = backends
         self.backends = backends
         self.writable = writable
         self.writable = writable
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of StackedConfig."""
         return f"<{self.__class__.__name__} for {self.backends!r}>"
         return f"<{self.__class__.__name__} for {self.backends!r}>"
 
 
     @classmethod
     @classmethod
     def default(cls) -> "StackedConfig":
     def default(cls) -> "StackedConfig":
+        """Create a StackedConfig with default system/user config files.
+
+        Returns:
+          StackedConfig with default configuration files loaded
+        """
         return cls(cls.default_backends())
         return cls(cls.default_backends())
 
 
     @classmethod
     @classmethod
@@ -1275,6 +1436,7 @@ class StackedConfig(Config):
         return backends
         return backends
 
 
     def get(self, section: SectionLike, name: NameLike) -> Value:
     def get(self, section: SectionLike, name: NameLike) -> Value:
+        """Get value from configuration."""
         if not isinstance(section, tuple):
         if not isinstance(section, tuple):
             section = (section,)
             section = (section,)
         for backend in self.backends:
         for backend in self.backends:
@@ -1285,6 +1447,7 @@ class StackedConfig(Config):
         raise KeyError(name)
         raise KeyError(name)
 
 
     def get_multivar(self, section: SectionLike, name: NameLike) -> Iterator[Value]:
     def get_multivar(self, section: SectionLike, name: NameLike) -> Iterator[Value]:
+        """Get multiple values from configuration."""
         if not isinstance(section, tuple):
         if not isinstance(section, tuple):
             section = (section,)
             section = (section,)
         for backend in self.backends:
         for backend in self.backends:
@@ -1296,11 +1459,13 @@ class StackedConfig(Config):
     def set(
     def set(
         self, section: SectionLike, name: NameLike, value: Union[ValueLike, bool]
         self, section: SectionLike, name: NameLike, value: Union[ValueLike, bool]
     ) -> None:
     ) -> None:
+        """Set value in configuration."""
         if self.writable is None:
         if self.writable is None:
             raise NotImplementedError(self.set)
             raise NotImplementedError(self.set)
         return self.writable.set(section, name, value)
         return self.writable.set(section, name, value)
 
 
     def sections(self) -> Iterator[Section]:
     def sections(self) -> Iterator[Section]:
+        """Get all sections."""
         seen = set()
         seen = set()
         for backend in self.backends:
         for backend in self.backends:
             for section in backend.sections():
             for section in backend.sections():

+ 6 - 2
dulwich/contrib/diffstat.py

@@ -122,8 +122,7 @@ def _parse_patch(
 # note must all done using bytes not string because on linux filenames
 # note must all done using bytes not string because on linux filenames
 # may not be encodable even to utf-8
 # may not be encodable even to utf-8
 def diffstat(lines: list[bytes], max_width: int = 80) -> bytes:
 def diffstat(lines: list[bytes], max_width: int = 80) -> bytes:
-    """Generate summary statistics from a git style diff ala
-       (git diff tag1 tag2 --stat).
+    """Generate summary statistics from a git style diff ala (git diff tag1 tag2 --stat).
 
 
     Args:
     Args:
       lines: list of byte string "lines" from the diff to be parsed
       lines: list of byte string "lines" from the diff to be parsed
@@ -200,6 +199,11 @@ def diffstat(lines: list[bytes], max_width: int = 80) -> bytes:
 
 
 
 
 def main() -> int:
 def main() -> int:
+    """Main entry point for diffstat command line tool.
+
+    Returns:
+      Exit code (0 for success)
+    """
     argv = sys.argv
     argv = sys.argv
     # allow diffstat.py to also be used from the command line
     # allow diffstat.py to also be used from the command line
     if len(sys.argv) > 1:
     if len(sys.argv) > 1:

+ 55 - 0
dulwich/contrib/paramiko_vendor.py

@@ -41,7 +41,15 @@ import paramiko.config
 
 
 
 
 class _ParamikoWrapper:
 class _ParamikoWrapper:
+    """Wrapper for paramiko SSH channel to provide a file-like interface."""
+
     def __init__(self, client: paramiko.SSHClient, channel: paramiko.Channel) -> None:
     def __init__(self, client: paramiko.SSHClient, channel: paramiko.Channel) -> None:
+        """Initialize the paramiko wrapper.
+
+        Args:
+            client: The SSH client instance
+            channel: The SSH channel for communication
+        """
         self.client = client
         self.client = client
         self.channel = channel
         self.channel = channel
 
 
@@ -50,15 +58,38 @@ class _ParamikoWrapper:
 
 
     @property
     @property
     def stderr(self) -> BinaryIO:
     def stderr(self) -> BinaryIO:
+        """Get stderr stream from the channel.
+
+        Returns:
+            Binary IO stream for stderr
+        """
         return cast(BinaryIO, self.channel.makefile_stderr("rb"))
         return cast(BinaryIO, self.channel.makefile_stderr("rb"))
 
 
     def can_read(self) -> bool:
     def can_read(self) -> bool:
+        """Check if data is available to read.
+
+        Returns:
+            True if data is available
+        """
         return self.channel.recv_ready()
         return self.channel.recv_ready()
 
 
     def write(self, data: bytes) -> None:
     def write(self, data: bytes) -> None:
+        """Write data to the channel.
+
+        Args:
+            data: Bytes to write
+        """
         return self.channel.sendall(data)
         return self.channel.sendall(data)
 
 
     def read(self, n: Optional[int] = None) -> bytes:
     def read(self, n: Optional[int] = None) -> bytes:
+        """Read data from the channel.
+
+        Args:
+            n: Number of bytes to read (default: 4096)
+
+        Returns:
+            Bytes read from the channel
+        """
         data = self.channel.recv(n or 4096)
         data = self.channel.recv(n or 4096)
         data_len = len(data)
         data_len = len(data)
 
 
@@ -73,13 +104,21 @@ class _ParamikoWrapper:
         return data
         return data
 
 
     def close(self) -> None:
     def close(self) -> None:
+        """Close the SSH channel."""
         self.channel.close()
         self.channel.close()
 
 
 
 
 class ParamikoSSHVendor:
 class ParamikoSSHVendor:
+    """SSH vendor implementation using paramiko."""
+
     # http://docs.paramiko.org/en/2.4/api/client.html
     # http://docs.paramiko.org/en/2.4/api/client.html
 
 
     def __init__(self, **kwargs: object) -> None:
     def __init__(self, **kwargs: object) -> None:
+        """Initialize the paramiko SSH vendor.
+
+        Args:
+            **kwargs: Additional keyword arguments passed to SSHClient
+        """
         self.kwargs = kwargs
         self.kwargs = kwargs
         self.ssh_config = self._load_ssh_config()
         self.ssh_config = self._load_ssh_config()
 
 
@@ -110,6 +149,22 @@ class ParamikoSSHVendor:
         protocol_version: Optional[int] = None,
         protocol_version: Optional[int] = None,
         **kwargs: object,
         **kwargs: object,
     ) -> _ParamikoWrapper:
     ) -> _ParamikoWrapper:
+        """Run a command on a remote host via SSH.
+
+        Args:
+            host: Hostname to connect to
+            command: Command to execute
+            username: SSH username (optional)
+            port: SSH port (optional)
+            password: SSH password (optional)
+            pkey: Private key for authentication (optional)
+            key_filename: Path to private key file (optional)
+            protocol_version: SSH protocol version (optional)
+            **kwargs: Additional keyword arguments
+
+        Returns:
+            _ParamikoWrapper instance for the SSH channel
+        """
         client = paramiko.SSHClient()
         client = paramiko.SSHClient()
 
 
         # Get SSH config for this host
         # Get SSH config for this host

+ 20 - 0
dulwich/contrib/requests_vendor.py

@@ -49,6 +49,8 @@ from ..errors import GitProtocolError, NotGitRepository
 
 
 
 
 class RequestsHttpGitClient(AbstractHttpGitClient):
 class RequestsHttpGitClient(AbstractHttpGitClient):
+    """HTTP Git client using the requests library."""
+
     def __init__(
     def __init__(
         self,
         self,
         base_url: str,
         base_url: str,
@@ -58,6 +60,16 @@ class RequestsHttpGitClient(AbstractHttpGitClient):
         password: Optional[str] = None,
         password: Optional[str] = None,
         **kwargs: object,
         **kwargs: object,
     ) -> None:
     ) -> None:
+        """Initialize RequestsHttpGitClient.
+
+        Args:
+          base_url: Base URL of the Git repository
+          dumb: Whether to use dumb HTTP transport
+          config: Git configuration file
+          username: Username for authentication
+          password: Password for authentication
+          **kwargs: Additional keyword arguments
+        """
         self._username = username
         self._username = username
         self._password = password
         self._password = password
 
 
@@ -112,6 +124,14 @@ class RequestsHttpGitClient(AbstractHttpGitClient):
 
 
 
 
 def get_session(config: Optional["ConfigFile"]) -> Session:
 def get_session(config: Optional["ConfigFile"]) -> Session:
+    """Create a requests session with Git configuration.
+
+    Args:
+      config: Git configuration file
+
+    Returns:
+      Configured requests Session
+    """
     session = Session()
     session = Session()
     session.headers.update({"Pragma": "no-cache"})
     session.headers.update({"Pragma": "no-cache"})
 
 

+ 126 - 4
dulwich/contrib/swift.py

@@ -97,7 +97,14 @@ cache_length = 20
 
 
 
 
 class PackInfoMissingObjectFinder(GreenThreadsMissingObjectFinder):
 class PackInfoMissingObjectFinder(GreenThreadsMissingObjectFinder):
+    """Find missing objects required for pack generation."""
+
     def next(self) -> Optional[tuple[bytes, int, Union[bytes, None]]]:
     def next(self) -> Optional[tuple[bytes, int, Union[bytes, None]]]:
+        """Get the next missing object.
+
+        Returns:
+          Tuple of (sha, pack_type_num, name) or None if no more objects
+        """
         while True:
         while True:
             if not self.objects_to_send:
             if not self.objects_to_send:
                 return None
                 return None
@@ -179,6 +186,15 @@ def swift_load_pack_index(scon: "SwiftConnector", filename: str) -> "PackIndex":
 
 
 
 
 def pack_info_create(pack_data: "PackData", pack_index: "PackIndex") -> bytes:
 def pack_info_create(pack_data: "PackData", pack_index: "PackIndex") -> bytes:
+    """Create pack info file contents.
+
+    Args:
+      pack_data: The pack data object
+      pack_index: The pack index object
+
+    Returns:
+      Compressed JSON bytes containing pack information
+    """
     pack = Pack.from_objects(pack_data, pack_index)
     pack = Pack.from_objects(pack_data, pack_index)
     info: dict = {}
     info: dict = {}
     for obj in pack.iterobjects():
     for obj in pack.iterobjects():
@@ -213,6 +229,16 @@ def load_pack_info(
     scon: Optional["SwiftConnector"] = None,
     scon: Optional["SwiftConnector"] = None,
     file: Optional[BinaryIO] = None,
     file: Optional[BinaryIO] = None,
 ) -> Optional[dict]:
 ) -> Optional[dict]:
+    """Load pack info from Swift or file.
+
+    Args:
+      filename: The pack info filename
+      scon: Optional Swift connector to use for loading
+      file: Optional file object to read from instead
+
+    Returns:
+      Dictionary containing pack information or None if not found
+    """
     if not file:
     if not file:
         if scon is None:
         if scon is None:
             return None
             return None
@@ -233,7 +259,7 @@ def load_pack_info(
 
 
 
 
 class SwiftException(Exception):
 class SwiftException(Exception):
-    pass
+    """Exception raised for Swift-related errors."""
 
 
 
 
 class SwiftConnector:
 class SwiftConnector:
@@ -281,6 +307,14 @@ class SwiftConnector:
         )
         )
 
 
     def swift_auth_v1(self) -> tuple[str, str]:
     def swift_auth_v1(self) -> tuple[str, str]:
+        """Authenticate with Swift using v1 authentication.
+
+        Returns:
+          Tuple of (storage_url, auth_token)
+
+        Raises:
+          SwiftException: If authentication fails
+        """
         self.user = self.user.replace(";", ":")
         self.user = self.user.replace(";", ":")
         auth_httpclient = HTTPClient.from_url(
         auth_httpclient = HTTPClient.from_url(
             self.auth_url,
             self.auth_url,
@@ -304,6 +338,14 @@ class SwiftConnector:
         return storage_url, token
         return storage_url, token
 
 
     def swift_auth_v2(self) -> tuple[str, str]:
     def swift_auth_v2(self) -> tuple[str, str]:
+        """Authenticate with Swift using v2 authentication.
+
+        Returns:
+          Tuple of (storage_url, auth_token)
+
+        Raises:
+          SwiftException: If authentication fails
+        """
         self.tenant, self.user = self.user.split(";")
         self.tenant, self.user = self.user.split(";")
         auth_dict = {}
         auth_dict = {}
         auth_dict["auth"] = {
         auth_dict["auth"] = {
@@ -615,6 +657,14 @@ class SwiftPackData(PackData):
     def get_object_at(
     def get_object_at(
         self, offset: int
         self, offset: int
     ) -> tuple[int, Union[tuple[Union[bytes, int], list[bytes]], list[bytes]]]:
     ) -> tuple[int, Union[tuple[Union[bytes, int], list[bytes]], list[bytes]]]:
+        """Get the object at a specific offset in the pack.
+
+        Args:
+          offset: The offset in the pack file
+
+        Returns:
+          Tuple of (pack_type_num, object_data)
+        """
         if offset in self._offset_cache:
         if offset in self._offset_cache:
             return self._offset_cache[offset]
             return self._offset_cache[offset]
         assert offset >= self._header_size
         assert offset >= self._header_size
@@ -625,11 +675,16 @@ class SwiftPackData(PackData):
         return (unpacked.pack_type_num, obj_data)
         return (unpacked.pack_type_num, obj_data)
 
 
     def get_stored_checksum(self) -> bytes:
     def get_stored_checksum(self) -> bytes:
+        """Get the stored checksum for this pack.
+
+        Returns:
+          The pack checksum as bytes
+        """
         pack_reader = SwiftPackReader(self.scon, str(self._filename), self.pack_length)
         pack_reader = SwiftPackReader(self.scon, str(self._filename), self.pack_length)
         return pack_reader.read_checksum()
         return pack_reader.read_checksum()
 
 
     def close(self) -> None:
     def close(self) -> None:
-        pass
+        """Close the pack data (no-op for Swift)."""
 
 
 
 
 class SwiftPack(Pack):
 class SwiftPack(Pack):
@@ -641,6 +696,12 @@ class SwiftPack(Pack):
     """
     """
 
 
     def __init__(self, *args: object, **kwargs: object) -> None:
     def __init__(self, *args: object, **kwargs: object) -> None:
+        """Initialize SwiftPack.
+
+        Args:
+          *args: Arguments to pass to parent class
+          **kwargs: Keyword arguments, must include 'scon' (SwiftConnector)
+        """
         self.scon = kwargs["scon"]
         self.scon = kwargs["scon"]
         del kwargs["scon"]
         del kwargs["scon"]
         super().__init__(*args, **kwargs)  # type: ignore
         super().__init__(*args, **kwargs)  # type: ignore
@@ -698,6 +759,14 @@ class SwiftObjectStore(PackBasedObjectStore):
         return iter([])
         return iter([])
 
 
     def pack_info_get(self, sha: bytes) -> Optional[tuple]:
     def pack_info_get(self, sha: bytes) -> Optional[tuple]:
+        """Get pack info for a specific SHA.
+
+        Args:
+          sha: The SHA to look up
+
+        Returns:
+          Pack info tuple or None if not found
+        """
         for pack in self.packs:
         for pack in self.packs:
             if sha in pack:
             if sha in pack:
                 if hasattr(pack, "pack_info"):
                 if hasattr(pack, "pack_info"):
@@ -748,6 +817,11 @@ class SwiftObjectStore(PackBasedObjectStore):
         f = BytesIO()
         f = BytesIO()
 
 
         def commit() -> Optional["SwiftPack"]:
         def commit() -> Optional["SwiftPack"]:
+            """Commit the pack to Swift storage.
+
+            Returns:
+              The created SwiftPack or None if empty
+            """
             f.seek(0)
             f.seek(0)
             pack = PackData(file=f, filename="")
             pack = PackData(file=f, filename="")
             entries = pack.sorted_entries()
             entries = pack.sorted_entries()
@@ -770,11 +844,16 @@ class SwiftObjectStore(PackBasedObjectStore):
                 return None
                 return None
 
 
         def abort() -> None:
         def abort() -> None:
-            pass
+            """Abort the pack operation (no-op)."""
 
 
         return f, commit, abort
         return f, commit, abort
 
 
     def add_object(self, obj: object) -> None:
     def add_object(self, obj: object) -> None:
+        """Add a single object to the store.
+
+        Args:
+          obj: The object to add
+        """
         self.add_objects(
         self.add_objects(
             [
             [
                 (obj, None),  # type: ignore
                 (obj, None),  # type: ignore
@@ -871,6 +950,12 @@ class SwiftInfoRefsContainer(InfoRefsContainer):
     """Manage references in info/refs object."""
     """Manage references in info/refs object."""
 
 
     def __init__(self, scon: SwiftConnector, store: object) -> None:
     def __init__(self, scon: SwiftConnector, store: object) -> None:
+        """Initialize SwiftInfoRefsContainer.
+
+        Args:
+          scon: Swift connector instance
+          store: Object store instance
+        """
         self.scon = scon
         self.scon = scon
         self.filename = "info/refs"
         self.filename = "info/refs"
         self.store = store
         self.store = store
@@ -946,6 +1031,11 @@ class SwiftInfoRefsContainer(InfoRefsContainer):
         return True
         return True
 
 
     def allkeys(self) -> Iterator[bytes]:
     def allkeys(self) -> Iterator[bytes]:
+        """Get all reference names.
+
+        Returns:
+          Iterator of reference names as bytes
+        """
         try:
         try:
             self._refs[b"HEAD"] = self._refs[b"refs/heads/master"]
             self._refs[b"HEAD"] = self._refs[b"refs/heads/master"]
         except KeyError:
         except KeyError:
@@ -954,6 +1044,8 @@ class SwiftInfoRefsContainer(InfoRefsContainer):
 
 
 
 
 class SwiftRepo(BaseRepo):
 class SwiftRepo(BaseRepo):
+    """A Git repository backed by Swift object storage."""
+
     def __init__(self, root: str, conf: ConfigParser) -> None:
     def __init__(self, root: str, conf: ConfigParser) -> None:
         """Init a Git bare Repository on top of a Swift container.
         """Init a Git bare Repository on top of a Swift container.
 
 
@@ -1020,17 +1112,37 @@ class SwiftRepo(BaseRepo):
 
 
 
 
 class SwiftSystemBackend(Backend):
 class SwiftSystemBackend(Backend):
+    """Backend for serving Git repositories from Swift."""
+
     def __init__(self, logger: "logging.Logger", conf: ConfigParser) -> None:
     def __init__(self, logger: "logging.Logger", conf: ConfigParser) -> None:
+        """Initialize SwiftSystemBackend.
+
+        Args:
+          logger: Logger instance
+          conf: Configuration parser instance
+        """
         self.conf = conf
         self.conf = conf
         self.logger = logger
         self.logger = logger
 
 
     def open_repository(self, path: str) -> "BackendRepo":
     def open_repository(self, path: str) -> "BackendRepo":
+        """Open a repository at the given path.
+
+        Args:
+          path: Path to the repository in Swift
+
+        Returns:
+          SwiftRepo instance
+        """
         self.logger.info("opening repository at %s", path)
         self.logger.info("opening repository at %s", path)
         return cast("BackendRepo", SwiftRepo(path, self.conf))
         return cast("BackendRepo", SwiftRepo(path, self.conf))
 
 
 
 
 def cmd_daemon(args: list) -> None:
 def cmd_daemon(args: list) -> None:
-    """Entry point for starting a TCP git server."""
+    """Start a TCP git server for Swift repositories.
+
+    Args:
+      args: Command line arguments
+    """
     import optparse
     import optparse
 
 
     parser = optparse.OptionParser()
     parser = optparse.OptionParser()
@@ -1082,6 +1194,11 @@ def cmd_daemon(args: list) -> None:
 
 
 
 
 def cmd_init(args: list) -> None:
 def cmd_init(args: list) -> None:
+    """Initialize a new Git repository in Swift.
+
+    Args:
+      args: Command line arguments
+    """
     import optparse
     import optparse
 
 
     parser = optparse.OptionParser()
     parser = optparse.OptionParser()
@@ -1103,6 +1220,11 @@ def cmd_init(args: list) -> None:
 
 
 
 
 def main(argv: list = sys.argv) -> None:
 def main(argv: list = sys.argv) -> None:
+    """Main entry point for Swift Git command line interface.
+
+    Args:
+      argv: Command line arguments
+    """
     commands = {
     commands = {
         "init": cmd_init,
         "init": cmd_init,
         "daemon": cmd_daemon,
         "daemon": cmd_daemon,

+ 9 - 0
dulwich/credentials.py

@@ -35,6 +35,15 @@ from .config import ConfigDict, SectionLike
 
 
 
 
 def match_urls(url: ParseResult, url_prefix: ParseResult) -> bool:
 def match_urls(url: ParseResult, url_prefix: ParseResult) -> bool:
+    """Check if a URL matches a URL prefix.
+
+    Args:
+      url: Parsed URL to check
+      url_prefix: Parsed URL prefix to match against
+
+    Returns:
+      True if url matches the prefix
+    """
     base_match = (
     base_match = (
         url.scheme == url_prefix.scheme
         url.scheme == url_prefix.scheme
         and url.hostname == url_prefix.hostname
         and url.hostname == url_prefix.hostname

+ 16 - 0
dulwich/diff_tree.py

@@ -54,10 +54,26 @@ class TreeChange(namedtuple("TreeChange", ["type", "old", "new"])):
 
 
     @classmethod
     @classmethod
     def add(cls, new: TreeEntry) -> "TreeChange":
     def add(cls, new: TreeEntry) -> "TreeChange":
+        """Create a TreeChange for an added entry.
+
+        Args:
+          new: New tree entry
+
+        Returns:
+          TreeChange instance
+        """
         return cls(CHANGE_ADD, _NULL_ENTRY, new)
         return cls(CHANGE_ADD, _NULL_ENTRY, new)
 
 
     @classmethod
     @classmethod
     def delete(cls, old: TreeEntry) -> "TreeChange":
     def delete(cls, old: TreeEntry) -> "TreeChange":
+        """Create a TreeChange for a deleted entry.
+
+        Args:
+          old: Old tree entry
+
+        Returns:
+          TreeChange instance
+        """
         return cls(CHANGE_DELETE, old, _NULL_ENTRY)
         return cls(CHANGE_DELETE, old, _NULL_ENTRY)
 
 
 
 

+ 6 - 0
dulwich/dumb.py

@@ -410,6 +410,11 @@ class DumbRemoteHTTPRepo:
         return dict(self._refs)
         return dict(self._refs)
 
 
     def get_head(self) -> Ref:
     def get_head(self) -> Ref:
+        """Get the current HEAD reference.
+
+        Returns:
+          HEAD reference name or commit ID
+        """
         head_resp_bytes = self._fetch_url("HEAD")
         head_resp_bytes = self._fetch_url("HEAD")
         head_split = head_resp_bytes.replace(b"\n", b"").split(b" ")
         head_split = head_resp_bytes.replace(b"\n", b"").split(b" ")
         head_target = head_split[1] if len(head_split) > 1 else head_split[0]
         head_target = head_split[1] if len(head_split) > 1 else head_split[0]
@@ -445,6 +450,7 @@ class DumbRemoteHTTPRepo:
           graph_walker: GraphWalker instance (not used for dumb HTTP)
           graph_walker: GraphWalker instance (not used for dumb HTTP)
           determine_wants: Function that returns list of wanted SHAs
           determine_wants: Function that returns list of wanted SHAs
           progress: Optional progress callback
           progress: Optional progress callback
+          get_tagged: Whether to get tagged objects
           depth: Depth for shallow clones (not supported for dumb HTTP)
           depth: Depth for shallow clones (not supported for dumb HTTP)
 
 
         Returns:
         Returns:

+ 72 - 0
dulwich/errors.py

@@ -39,6 +39,13 @@ class ChecksumMismatch(Exception):
         got: Union[bytes, str],
         got: Union[bytes, str],
         extra: Optional[str] = None,
         extra: Optional[str] = None,
     ) -> None:
     ) -> None:
+        """Initialize a ChecksumMismatch exception.
+
+        Args:
+            expected: The expected checksum value (bytes or hex string).
+            got: The actual checksum value (bytes or hex string).
+            extra: Optional additional error information.
+        """
         if isinstance(expected, bytes) and len(expected) == 20:
         if isinstance(expected, bytes) and len(expected) == 20:
             expected_str = binascii.hexlify(expected).decode("ascii")
             expected_str = binascii.hexlify(expected).decode("ascii")
         else:
         else:
@@ -70,6 +77,13 @@ class WrongObjectException(Exception):
     type_name: str
     type_name: str
 
 
     def __init__(self, sha: bytes, *args: object, **kwargs: object) -> None:
     def __init__(self, sha: bytes, *args: object, **kwargs: object) -> None:
+        """Initialize a WrongObjectException.
+
+        Args:
+            sha: The SHA of the object that was not of the expected type.
+            *args: Additional positional arguments.
+            **kwargs: Additional keyword arguments.
+        """
         Exception.__init__(self, f"{sha.decode('ascii')} is not a {self.type_name}")
         Exception.__init__(self, f"{sha.decode('ascii')} is not a {self.type_name}")
 
 
 
 
@@ -101,6 +115,13 @@ class MissingCommitError(Exception):
     """Indicates that a commit was not found in the repository."""
     """Indicates that a commit was not found in the repository."""
 
 
     def __init__(self, sha: bytes, *args: object, **kwargs: object) -> None:
     def __init__(self, sha: bytes, *args: object, **kwargs: object) -> None:
+        """Initialize a MissingCommitError.
+
+        Args:
+            sha: The SHA of the missing commit.
+            *args: Additional positional arguments.
+            **kwargs: Additional keyword arguments.
+        """
         self.sha = sha
         self.sha = sha
         Exception.__init__(self, f"{sha.decode('ascii')} is not in the revision store")
         Exception.__init__(self, f"{sha.decode('ascii')} is not in the revision store")
 
 
@@ -109,6 +130,13 @@ class ObjectMissing(Exception):
     """Indicates that a requested object is missing."""
     """Indicates that a requested object is missing."""
 
 
     def __init__(self, sha: bytes, *args: object, **kwargs: object) -> None:
     def __init__(self, sha: bytes, *args: object, **kwargs: object) -> None:
+        """Initialize an ObjectMissing exception.
+
+        Args:
+            sha: The SHA of the missing object.
+            *args: Additional positional arguments.
+            **kwargs: Additional keyword arguments.
+        """
         Exception.__init__(self, f"{sha.decode('ascii')} is not in the pack")
         Exception.__init__(self, f"{sha.decode('ascii')} is not in the pack")
 
 
 
 
@@ -116,6 +144,12 @@ class ApplyDeltaError(Exception):
     """Indicates that applying a delta failed."""
     """Indicates that applying a delta failed."""
 
 
     def __init__(self, *args: object, **kwargs: object) -> None:
     def __init__(self, *args: object, **kwargs: object) -> None:
+        """Initialize an ApplyDeltaError.
+
+        Args:
+            *args: Error message and additional positional arguments.
+            **kwargs: Additional keyword arguments.
+        """
         Exception.__init__(self, *args, **kwargs)
         Exception.__init__(self, *args, **kwargs)
 
 
 
 
@@ -123,6 +157,12 @@ class NotGitRepository(Exception):
     """Indicates that no Git repository was found."""
     """Indicates that no Git repository was found."""
 
 
     def __init__(self, *args: object, **kwargs: object) -> None:
     def __init__(self, *args: object, **kwargs: object) -> None:
+        """Initialize a NotGitRepository exception.
+
+        Args:
+            *args: Error message and additional positional arguments.
+            **kwargs: Additional keyword arguments.
+        """
         Exception.__init__(self, *args, **kwargs)
         Exception.__init__(self, *args, **kwargs)
 
 
 
 
@@ -130,9 +170,23 @@ class GitProtocolError(Exception):
     """Git protocol exception."""
     """Git protocol exception."""
 
 
     def __init__(self, *args: object, **kwargs: object) -> None:
     def __init__(self, *args: object, **kwargs: object) -> None:
+        """Initialize a GitProtocolError.
+
+        Args:
+            *args: Error message and additional positional arguments.
+            **kwargs: Additional keyword arguments.
+        """
         Exception.__init__(self, *args, **kwargs)
         Exception.__init__(self, *args, **kwargs)
 
 
     def __eq__(self, other: object) -> bool:
     def __eq__(self, other: object) -> bool:
+        """Check equality between GitProtocolError instances.
+
+        Args:
+            other: The object to compare with.
+
+        Returns:
+            True if both are GitProtocolError instances with same args, False otherwise.
+        """
         return isinstance(other, GitProtocolError) and self.args == other.args
         return isinstance(other, GitProtocolError) and self.args == other.args
 
 
 
 
@@ -144,6 +198,11 @@ class HangupException(GitProtocolError):
     """Hangup exception."""
     """Hangup exception."""
 
 
     def __init__(self, stderr_lines: Optional[list[bytes]] = None) -> None:
     def __init__(self, stderr_lines: Optional[list[bytes]] = None) -> None:
+        """Initialize a HangupException.
+
+        Args:
+            stderr_lines: Optional list of stderr output lines from the remote server.
+        """
         if stderr_lines:
         if stderr_lines:
             super().__init__(
             super().__init__(
                 "\n".join(
                 "\n".join(
@@ -155,6 +214,14 @@ class HangupException(GitProtocolError):
         self.stderr_lines = stderr_lines
         self.stderr_lines = stderr_lines
 
 
     def __eq__(self, other: object) -> bool:
     def __eq__(self, other: object) -> bool:
+        """Check equality between HangupException instances.
+
+        Args:
+            other: The object to compare with.
+
+        Returns:
+            True if both are HangupException instances with same stderr_lines, False otherwise.
+        """
         return (
         return (
             isinstance(other, HangupException)
             isinstance(other, HangupException)
             and self.stderr_lines == other.stderr_lines
             and self.stderr_lines == other.stderr_lines
@@ -165,6 +232,11 @@ class UnexpectedCommandError(GitProtocolError):
     """Unexpected command received in a proto line."""
     """Unexpected command received in a proto line."""
 
 
     def __init__(self, command: Optional[str]) -> None:
     def __init__(self, command: Optional[str]) -> None:
+        """Initialize an UnexpectedCommandError.
+
+        Args:
+            command: The unexpected command received, or None for flush-pkt.
+        """
         command_str = "flush-pkt" if command is None else f"command {command}"
         command_str = "flush-pkt" if command is None else f"command {command}"
         super().__init__(f"Protocol got unexpected {command_str}")
         super().__init__(f"Protocol got unexpected {command_str}")
 
 

+ 74 - 0
dulwich/fastexport.py

@@ -40,6 +40,14 @@ if TYPE_CHECKING:
 
 
 
 
 def split_email(text: bytes) -> tuple[bytes, bytes]:
 def split_email(text: bytes) -> tuple[bytes, bytes]:
+    """Split email address from name.
+
+    Args:
+        text: Full name and email (e.g. b"John Doe <john@example.com>")
+
+    Returns:
+        Tuple of (name, email)
+    """
     # TODO(jelmer): Dedupe this and the same functionality in
     # TODO(jelmer): Dedupe this and the same functionality in
     # format_annotate_line.
     # format_annotate_line.
     (name, email) = text.rsplit(b" <", 1)
     (name, email) = text.rsplit(b" <", 1)
@@ -50,12 +58,23 @@ class GitFastExporter:
     """Generate a fast-export output stream for Git objects."""
     """Generate a fast-export output stream for Git objects."""
 
 
     def __init__(self, outf: BinaryIO, store: "BaseObjectStore") -> None:
     def __init__(self, outf: BinaryIO, store: "BaseObjectStore") -> None:
+        """Initialize the fast exporter.
+
+        Args:
+            outf: Output file to write to
+            store: Object store to export from
+        """
         self.outf = outf
         self.outf = outf
         self.store = store
         self.store = store
         self.markers: dict[bytes, bytes] = {}
         self.markers: dict[bytes, bytes] = {}
         self._marker_idx = 0
         self._marker_idx = 0
 
 
     def print_cmd(self, cmd: object) -> None:
     def print_cmd(self, cmd: object) -> None:
+        """Print a command to the output stream.
+
+        Args:
+            cmd: Command object to print
+        """
         if hasattr(cmd, "__bytes__"):
         if hasattr(cmd, "__bytes__"):
             output = cmd.__bytes__()
             output = cmd.__bytes__()
         else:
         else:
@@ -63,15 +82,36 @@ class GitFastExporter:
         self.outf.write(output + b"\n")
         self.outf.write(output + b"\n")
 
 
     def _allocate_marker(self) -> bytes:
     def _allocate_marker(self) -> bytes:
+        """Allocate a new marker.
+
+        Returns:
+            New marker as bytes
+        """
         self._marker_idx += 1
         self._marker_idx += 1
         return str(self._marker_idx).encode("ascii")
         return str(self._marker_idx).encode("ascii")
 
 
     def _export_blob(self, blob: Blob) -> tuple[Any, bytes]:
     def _export_blob(self, blob: Blob) -> tuple[Any, bytes]:
+        """Export a blob object.
+
+        Args:
+            blob: Blob object to export
+
+        Returns:
+            Tuple of (BlobCommand, marker)
+        """
         marker = self._allocate_marker()
         marker = self._allocate_marker()
         self.markers[marker] = blob.id
         self.markers[marker] = blob.id
         return (commands.BlobCommand(marker, blob.data), marker)
         return (commands.BlobCommand(marker, blob.data), marker)
 
 
     def emit_blob(self, blob: Blob) -> bytes:
     def emit_blob(self, blob: Blob) -> bytes:
+        """Emit a blob to the output stream.
+
+        Args:
+            blob: Blob object to emit
+
+        Returns:
+            Marker for the blob
+        """
         (cmd, marker) = self._export_blob(blob)
         (cmd, marker) = self._export_blob(blob)
         self.print_cmd(cmd)
         self.print_cmd(cmd)
         return marker
         return marker
@@ -137,6 +177,16 @@ class GitFastExporter:
     def emit_commit(
     def emit_commit(
         self, commit: Commit, ref: Ref, base_tree: Optional[ObjectID] = None
         self, commit: Commit, ref: Ref, base_tree: Optional[ObjectID] = None
     ) -> bytes:
     ) -> bytes:
+        """Emit a commit in fast-export format.
+
+        Args:
+          commit: Commit object to export
+          ref: Reference name for the commit
+          base_tree: Base tree for incremental export
+
+        Returns:
+          Marker for the commit
+        """
         cmd, marker = self._export_commit(commit, ref, base_tree)
         cmd, marker = self._export_commit(commit, ref, base_tree)
         self.print_cmd(cmd)
         self.print_cmd(cmd)
         return marker
         return marker
@@ -154,6 +204,14 @@ class GitImportProcessor(processor.ImportProcessor):
         verbose: bool = False,
         verbose: bool = False,
         outf: Optional[BinaryIO] = None,
         outf: Optional[BinaryIO] = None,
     ) -> None:
     ) -> None:
+        """Initialize GitImportProcessor.
+
+        Args:
+          repo: Repository to import into
+          params: Import parameters
+          verbose: Whether to enable verbose output
+          outf: Output file for verbose messages
+        """
         processor.ImportProcessor.__init__(self, params, verbose)
         processor.ImportProcessor.__init__(self, params, verbose)
         self.repo = repo
         self.repo = repo
         self.last_commit = ZERO_SHA
         self.last_commit = ZERO_SHA
@@ -161,11 +219,27 @@ class GitImportProcessor(processor.ImportProcessor):
         self._contents: dict[bytes, tuple[int, bytes]] = {}
         self._contents: dict[bytes, tuple[int, bytes]] = {}
 
 
     def lookup_object(self, objectish: bytes) -> ObjectID:
     def lookup_object(self, objectish: bytes) -> ObjectID:
+        """Look up an object by reference or marker.
+
+        Args:
+          objectish: Object reference or marker
+
+        Returns:
+          Object ID
+        """
         if objectish.startswith(b":"):
         if objectish.startswith(b":"):
             return self.markers[objectish[1:]]
             return self.markers[objectish[1:]]
         return objectish
         return objectish
 
 
     def import_stream(self, stream: BinaryIO) -> dict[bytes, bytes]:
     def import_stream(self, stream: BinaryIO) -> dict[bytes, bytes]:
+        """Import from a fast-import stream.
+
+        Args:
+          stream: Stream to import from
+
+        Returns:
+          Dictionary of markers to object IDs
+        """
         p = parser.ImportParser(stream)
         p = parser.ImportParser(stream)
         self.process(p.iter_commands)
         self.process(p.iter_commands)
         return self.markers
         return self.markers

+ 6 - 0
dulwich/file.py

@@ -125,6 +125,12 @@ class FileLocked(Exception):
     def __init__(
     def __init__(
         self, filename: Union[str, bytes, os.PathLike], lockfilename: Union[str, bytes]
         self, filename: Union[str, bytes, os.PathLike], lockfilename: Union[str, bytes]
     ) -> None:
     ) -> None:
+        """Initialize FileLocked.
+
+        Args:
+          filename: Name of the file that is locked
+          lockfilename: Name of the lock file
+        """
         self.filename = filename
         self.filename = filename
         self.lockfilename = lockfilename
         self.lockfilename = lockfilename
         super().__init__(filename, lockfilename)
         super().__init__(filename, lockfilename)

+ 22 - 0
dulwich/filters.py

@@ -59,6 +59,14 @@ class ProcessFilterDriver:
         required: bool = False,
         required: bool = False,
         cwd: Optional[str] = None,
         cwd: Optional[str] = None,
     ) -> None:
     ) -> None:
+        """Initialize ProcessFilterDriver.
+
+        Args:
+          clean_cmd: Command to run for clean filter
+          smudge_cmd: Command to run for smudge filter
+          required: Whether the filter is required
+          cwd: Working directory for filter execution
+        """
         self.clean_cmd = clean_cmd
         self.clean_cmd = clean_cmd
         self.smudge_cmd = smudge_cmd
         self.smudge_cmd = smudge_cmd
         self.required = required
         self.required = required
@@ -122,6 +130,12 @@ class FilterRegistry:
     def __init__(
     def __init__(
         self, config: Optional["StackedConfig"] = None, repo: Optional["Repo"] = None
         self, config: Optional["StackedConfig"] = None, repo: Optional["Repo"] = None
     ) -> None:
     ) -> None:
+        """Initialize FilterRegistry.
+
+        Args:
+          config: Git configuration stack
+          repo: Repository instance
+        """
         self.config = config
         self.config = config
         self.repo = repo
         self.repo = repo
         self._drivers: dict[str, FilterDriver] = {}
         self._drivers: dict[str, FilterDriver] = {}
@@ -377,6 +391,14 @@ class FilterBlobNormalizer:
         filter_registry: Optional[FilterRegistry] = None,
         filter_registry: Optional[FilterRegistry] = None,
         repo: Optional["Repo"] = None,
         repo: Optional["Repo"] = None,
     ) -> None:
     ) -> None:
+        """Initialize FilterBlobNormalizer.
+
+        Args:
+          config_stack: Git configuration stack
+          gitattributes: GitAttributes instance
+          filter_registry: Optional filter registry to use
+          repo: Optional repository instance
+        """
         self.config_stack = config_stack
         self.config_stack = config_stack
         self.gitattributes = gitattributes
         self.gitattributes = gitattributes
         self.filter_registry = filter_registry or FilterRegistry(config_stack, repo)
         self.filter_registry = filter_registry or FilterRegistry(config_stack, repo)

+ 40 - 0
dulwich/graph.py

@@ -37,14 +37,27 @@ T = TypeVar("T")
 # why they do not have a builtin maxheap is simply ridiculous but
 # why they do not have a builtin maxheap is simply ridiculous but
 # liveable with integer time stamps using negation
 # liveable with integer time stamps using negation
 class WorkList(Generic[T]):
 class WorkList(Generic[T]):
+    """Priority queue for commit processing using a min-heap."""
+
     def __init__(self) -> None:
     def __init__(self) -> None:
+        """Initialize an empty work list."""
         self.pq: list[tuple[int, T]] = []
         self.pq: list[tuple[int, T]] = []
 
 
     def add(self, item: tuple[int, T]) -> None:
     def add(self, item: tuple[int, T]) -> None:
+        """Add an item to the work list.
+
+        Args:
+            item: Tuple of (timestamp, commit)
+        """
         dt, cmt = item
         dt, cmt = item
         heappush(self.pq, (-dt, cmt))
         heappush(self.pq, (-dt, cmt))
 
 
     def get(self) -> Optional[tuple[int, T]]:
     def get(self) -> Optional[tuple[int, T]]:
+        """Get the highest priority item from the work list.
+
+        Returns:
+            Tuple of (timestamp, commit) or None if empty
+        """
         item = heappop(self.pq)
         item = heappop(self.pq)
         if item:
         if item:
             pr, cmt = item
             pr, cmt = item
@@ -52,6 +65,11 @@ class WorkList(Generic[T]):
         return None
         return None
 
 
     def iter(self) -> Iterator[tuple[int, T]]:
     def iter(self) -> Iterator[tuple[int, T]]:
+        """Iterate over items in the work list.
+
+        Yields:
+            Tuples of (timestamp, commit)
+        """
         for pr, cmt in self.pq:
         for pr, cmt in self.pq:
             yield (-pr, cmt)
             yield (-pr, cmt)
 
 
@@ -64,6 +82,19 @@ def _find_lcas(
     min_stamp: int = 0,
     min_stamp: int = 0,
     shallows: Optional[set[ObjectID]] = None,
     shallows: Optional[set[ObjectID]] = None,
 ) -> list[ObjectID]:
 ) -> list[ObjectID]:
+    """Find lowest common ancestors between commits.
+
+    Args:
+        lookup_parents: Function to get parent commits
+        c1: First commit
+        c2s: List of second commits
+        lookup_stamp: Function to get commit timestamp
+        min_stamp: Minimum timestamp to consider
+        shallows: Set of shallow commits
+
+    Returns:
+        List of lowest common ancestor commit IDs
+    """
     cands = []
     cands = []
     cstates = {}
     cstates = {}
 
 
@@ -74,6 +105,15 @@ def _find_lcas(
     _LCA = 8  # potential LCA (Lowest Common Ancestor)
     _LCA = 8  # potential LCA (Lowest Common Ancestor)
 
 
     def _has_candidates(wlst: WorkList[ObjectID], cstates: dict[ObjectID, int]) -> bool:
     def _has_candidates(wlst: WorkList[ObjectID], cstates: dict[ObjectID, int]) -> bool:
+        """Check if there are any candidate commits in the work list.
+
+        Args:
+            wlst: Work list of commits
+            cstates: Dictionary of commit states
+
+        Returns:
+            True if there are candidates to process
+        """
         for dt, cmt in wlst.iter():
         for dt, cmt in wlst.iter():
             if cmt in cstates:
             if cmt in cstates:
                 if not ((cstates[cmt] & _DNC) == _DNC):
                 if not ((cstates[cmt] & _DNC) == _DNC):

+ 12 - 0
dulwich/greenthreads.py

@@ -89,6 +89,18 @@ class GreenThreadsMissingObjectFinder(MissingObjectFinder):
         concurrency: int = 1,
         concurrency: int = 1,
         get_parents: Optional[Callable[[ObjectID], list[ObjectID]]] = None,
         get_parents: Optional[Callable[[ObjectID], list[ObjectID]]] = None,
     ) -> None:
     ) -> None:
+        """Initialize GreenThreadsMissingObjectFinder.
+
+        Args:
+          object_store: Object store to search
+          haves: Objects we have
+          wants: Objects we want
+          progress: Optional progress callback
+          get_tagged: Optional function to get tagged objects
+          concurrency: Number of concurrent green threads
+          get_parents: Optional function to get commit parents
+        """
+
         def collect_tree_sha(sha: ObjectID) -> None:
         def collect_tree_sha(sha: ObjectID) -> None:
             self.sha_done.add(sha)
             self.sha_done.add(sha)
             obj = object_store[sha]
             obj = object_store[sha]

+ 33 - 0
dulwich/hooks.py

@@ -115,6 +115,12 @@ class PreCommitShellHook(ShellHook):
     """pre-commit shell hook."""
     """pre-commit shell hook."""
 
 
     def __init__(self, cwd: str, controldir: str) -> None:
     def __init__(self, cwd: str, controldir: str) -> None:
+        """Initialize pre-commit hook.
+
+        Args:
+            cwd: Working directory for hook execution
+            controldir: Path to the git control directory (.git)
+        """
         filepath = os.path.join(controldir, "hooks", "pre-commit")
         filepath = os.path.join(controldir, "hooks", "pre-commit")
 
 
         ShellHook.__init__(self, "pre-commit", filepath, 0, cwd=cwd)
         ShellHook.__init__(self, "pre-commit", filepath, 0, cwd=cwd)
@@ -124,6 +130,11 @@ class PostCommitShellHook(ShellHook):
     """post-commit shell hook."""
     """post-commit shell hook."""
 
 
     def __init__(self, controldir: str) -> None:
     def __init__(self, controldir: str) -> None:
+        """Initialize post-commit hook.
+
+        Args:
+            controldir: Path to the git control directory (.git)
+        """
         filepath = os.path.join(controldir, "hooks", "post-commit")
         filepath = os.path.join(controldir, "hooks", "post-commit")
 
 
         ShellHook.__init__(self, "post-commit", filepath, 0, cwd=controldir)
         ShellHook.__init__(self, "post-commit", filepath, 0, cwd=controldir)
@@ -133,6 +144,11 @@ class CommitMsgShellHook(ShellHook):
     """commit-msg shell hook."""
     """commit-msg shell hook."""
 
 
     def __init__(self, controldir: str) -> None:
     def __init__(self, controldir: str) -> None:
+        """Initialize commit-msg hook.
+
+        Args:
+            controldir: Path to the git control directory (.git)
+        """
         filepath = os.path.join(controldir, "hooks", "commit-msg")
         filepath = os.path.join(controldir, "hooks", "commit-msg")
 
 
         def prepare_msg(*args: bytes) -> tuple[str, ...]:
         def prepare_msg(*args: bytes) -> tuple[str, ...]:
@@ -163,11 +179,28 @@ class PostReceiveShellHook(ShellHook):
     """post-receive shell hook."""
     """post-receive shell hook."""
 
 
     def __init__(self, controldir: str) -> None:
     def __init__(self, controldir: str) -> None:
+        """Initialize post-receive hook.
+
+        Args:
+            controldir: Path to the git control directory (.git)
+        """
         self.controldir = controldir
         self.controldir = controldir
         filepath = os.path.join(controldir, "hooks", "post-receive")
         filepath = os.path.join(controldir, "hooks", "post-receive")
         ShellHook.__init__(self, "post-receive", path=filepath, numparam=0)
         ShellHook.__init__(self, "post-receive", path=filepath, numparam=0)
 
 
     def execute(self, client_refs: list[tuple[bytes, bytes, bytes]]) -> Optional[bytes]:
     def execute(self, client_refs: list[tuple[bytes, bytes, bytes]]) -> Optional[bytes]:
+        """Execute the post-receive hook.
+
+        Args:
+            client_refs: List of tuples containing (old_sha, new_sha, ref_name)
+                        for each updated reference
+
+        Returns:
+            Output from the hook execution or None if hook doesn't exist
+
+        Raises:
+            HookError: If hook execution fails
+        """
         # do nothing if the script doesn't exist
         # do nothing if the script doesn't exist
         if not os.path.exists(self.filepath):
         if not os.path.exists(self.filepath):
             return None
             return None

+ 67 - 0
dulwich/ignore.py

@@ -308,6 +308,12 @@ class Pattern:
     """A single ignore pattern."""
     """A single ignore pattern."""
 
 
     def __init__(self, pattern: bytes, ignorecase: bool = False) -> None:
     def __init__(self, pattern: bytes, ignorecase: bool = False) -> None:
+        """Initialize a Pattern object.
+
+        Args:
+            pattern: The gitignore pattern as bytes.
+            ignorecase: Whether to perform case-insensitive matching.
+        """
         self.pattern = pattern
         self.pattern = pattern
         self.ignorecase = ignorecase
         self.ignorecase = ignorecase
 
 
@@ -334,12 +340,30 @@ class Pattern:
         self._re = re.compile(translate(pattern), flags)
         self._re = re.compile(translate(pattern), flags)
 
 
     def __bytes__(self) -> bytes:
     def __bytes__(self) -> bytes:
+        """Return the pattern as bytes.
+
+        Returns:
+            The original pattern as bytes.
+        """
         return self.pattern
         return self.pattern
 
 
     def __str__(self) -> str:
     def __str__(self) -> str:
+        """Return the pattern as a string.
+
+        Returns:
+            The pattern decoded as a string.
+        """
         return os.fsdecode(self.pattern)
         return os.fsdecode(self.pattern)
 
 
     def __eq__(self, other: object) -> bool:
     def __eq__(self, other: object) -> bool:
+        """Check equality with another Pattern object.
+
+        Args:
+            other: The object to compare with.
+
+        Returns:
+            True if patterns and ignorecase flags are equal, False otherwise.
+        """
         return (
         return (
             isinstance(other, type(self))
             isinstance(other, type(self))
             and self.pattern == other.pattern
             and self.pattern == other.pattern
@@ -347,6 +371,11 @@ class Pattern:
         )
         )
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return a string representation of the Pattern object.
+
+        Returns:
+            A string representation for debugging.
+        """
         return f"{type(self).__name__}({self.pattern!r}, {self.ignorecase!r})"
         return f"{type(self).__name__}({self.pattern!r}, {self.ignorecase!r})"
 
 
     def match(self, path: bytes) -> bool:
     def match(self, path: bytes) -> bool:
@@ -389,6 +418,13 @@ class IgnoreFilter:
         ignorecase: bool = False,
         ignorecase: bool = False,
         path: Optional[str] = None,
         path: Optional[str] = None,
     ) -> None:
     ) -> None:
+        """Initialize an IgnoreFilter with a set of patterns.
+
+        Args:
+            patterns: An iterable of gitignore patterns as bytes.
+            ignorecase: Whether to perform case-insensitive matching.
+            path: Optional path to the ignore file for debugging purposes.
+        """
         self._patterns: list[Pattern] = []
         self._patterns: list[Pattern] = []
         self._ignorecase = ignorecase
         self._ignorecase = ignorecase
         self._path = path
         self._path = path
@@ -450,10 +486,20 @@ class IgnoreFilter:
     def from_path(
     def from_path(
         cls, path: Union[str, os.PathLike], ignorecase: bool = False
         cls, path: Union[str, os.PathLike], ignorecase: bool = False
     ) -> "IgnoreFilter":
     ) -> "IgnoreFilter":
+        """Create an IgnoreFilter from a file path.
+
+        Args:
+            path: Path to the ignore file.
+            ignorecase: Whether to perform case-insensitive matching.
+
+        Returns:
+            An IgnoreFilter instance with patterns loaded from the file.
+        """
         with open(path, "rb") as f:
         with open(path, "rb") as f:
             return cls(read_ignore_patterns(f), ignorecase, path=str(path))
             return cls(read_ignore_patterns(f), ignorecase, path=str(path))
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of IgnoreFilter."""
         path = getattr(self, "_path", None)
         path = getattr(self, "_path", None)
         if path is not None:
         if path is not None:
             return f"{type(self).__name__}.from_path({path!r})"
             return f"{type(self).__name__}.from_path({path!r})"
@@ -465,6 +511,11 @@ class IgnoreFilterStack:
     """Check for ignore status in multiple filters."""
     """Check for ignore status in multiple filters."""
 
 
     def __init__(self, filters: list[IgnoreFilter]) -> None:
     def __init__(self, filters: list[IgnoreFilter]) -> None:
+        """Initialize an IgnoreFilterStack with multiple filters.
+
+        Args:
+            filters: A list of IgnoreFilter objects to check in order.
+        """
         self._filters = filters
         self._filters = filters
 
 
     def is_ignored(self, path: str) -> Optional[bool]:
     def is_ignored(self, path: str) -> Optional[bool]:
@@ -482,6 +533,14 @@ class IgnoreFilterStack:
                 return status
                 return status
         return None
         return None
 
 
+    def __repr__(self) -> str:
+        """Return a string representation of the IgnoreFilterStack.
+
+        Returns:
+            A string representation for debugging.
+        """
+        return f"{type(self).__name__}({self._filters!r})"
+
 
 
 def default_user_ignore_filter_path(config: Config) -> str:
 def default_user_ignore_filter_path(config: Config) -> str:
     """Return default user ignore filter path.
     """Return default user ignore filter path.
@@ -514,12 +573,20 @@ class IgnoreFilterManager:
         global_filters: list[IgnoreFilter],
         global_filters: list[IgnoreFilter],
         ignorecase: bool,
         ignorecase: bool,
     ) -> None:
     ) -> None:
+        """Initialize an IgnoreFilterManager.
+
+        Args:
+            top_path: The top-level directory path to manage ignores for.
+            global_filters: List of global ignore filters to apply.
+            ignorecase: Whether to perform case-insensitive matching.
+        """
         self._path_filters: dict[str, Optional[IgnoreFilter]] = {}
         self._path_filters: dict[str, Optional[IgnoreFilter]] = {}
         self._top_path = top_path
         self._top_path = top_path
         self._global_filters = global_filters
         self._global_filters = global_filters
         self._ignorecase = ignorecase
         self._ignorecase = ignorecase
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of IgnoreFilterManager."""
         return f"{type(self).__name__}({self._top_path}, {self._global_filters!r}, {self._ignorecase!r})"
         return f"{type(self).__name__}({self._top_path}, {self._global_filters!r}, {self._ignorecase!r})"
 
 
     def _load_path(self, path: str) -> Optional[IgnoreFilter]:
     def _load_path(self, path: str) -> Optional[IgnoreFilter]:

+ 195 - 2
dulwich/index.py

@@ -259,6 +259,8 @@ def _decompress_path_from_stream(
 
 
 
 
 class Stage(Enum):
 class Stage(Enum):
+    """Represents the stage of an index entry during merge conflicts."""
+
     NORMAL = 0
     NORMAL = 0
     MERGE_CONFLICT_ANCESTOR = 1
     MERGE_CONFLICT_ANCESTOR = 1
     MERGE_CONFLICT_THIS = 2
     MERGE_CONFLICT_THIS = 2
@@ -267,6 +269,12 @@ class Stage(Enum):
 
 
 @dataclass
 @dataclass
 class SerializedIndexEntry:
 class SerializedIndexEntry:
+    """Represents a serialized index entry as stored in the index file.
+
+    This dataclass holds the raw data for an index entry before it's
+    parsed into the more user-friendly IndexEntry format.
+    """
+
     name: bytes
     name: bytes
     ctime: Union[int, float, tuple[int, int]]
     ctime: Union[int, float, tuple[int, int]]
     mtime: Union[int, float, tuple[int, int]]
     mtime: Union[int, float, tuple[int, int]]
@@ -281,6 +289,11 @@ class SerializedIndexEntry:
     extended_flags: int
     extended_flags: int
 
 
     def stage(self) -> Stage:
     def stage(self) -> Stage:
+        """Extract the stage from the flags field.
+
+        Returns:
+          Stage enum value indicating merge conflict state
+        """
         return Stage((self.flags & FLAG_STAGEMASK) >> FLAG_STAGESHIFT)
         return Stage((self.flags & FLAG_STAGEMASK) >> FLAG_STAGESHIFT)
 
 
 
 
@@ -320,15 +333,33 @@ class TreeExtension(IndexExtension):
     """Tree cache extension."""
     """Tree cache extension."""
 
 
     def __init__(self, entries: list[tuple[bytes, bytes, int]]) -> None:
     def __init__(self, entries: list[tuple[bytes, bytes, int]]) -> None:
+        """Initialize TreeExtension.
+
+        Args:
+            entries: List of tree cache entries (path, sha, flags)
+        """
         self.entries = entries
         self.entries = entries
         super().__init__(TREE_EXTENSION, b"")
         super().__init__(TREE_EXTENSION, b"")
 
 
     @classmethod
     @classmethod
     def from_bytes(cls, data: bytes) -> "TreeExtension":
     def from_bytes(cls, data: bytes) -> "TreeExtension":
+        """Parse TreeExtension from bytes.
+
+        Args:
+          data: Raw bytes to parse
+
+        Returns:
+          TreeExtension instance
+        """
         # TODO: Implement tree cache parsing
         # TODO: Implement tree cache parsing
         return cls([])
         return cls([])
 
 
     def to_bytes(self) -> bytes:
     def to_bytes(self) -> bytes:
+        """Serialize TreeExtension to bytes.
+
+        Returns:
+          Serialized extension data
+        """
         # TODO: Implement tree cache serialization
         # TODO: Implement tree cache serialization
         return b""
         return b""
 
 
@@ -337,15 +368,33 @@ class ResolveUndoExtension(IndexExtension):
     """Resolve undo extension for recording merge conflicts."""
     """Resolve undo extension for recording merge conflicts."""
 
 
     def __init__(self, entries: list[tuple[bytes, list[tuple[int, bytes]]]]) -> None:
     def __init__(self, entries: list[tuple[bytes, list[tuple[int, bytes]]]]) -> None:
+        """Initialize ResolveUndoExtension.
+
+        Args:
+            entries: List of (path, stages) where stages is a list of (stage, sha) tuples
+        """
         self.entries = entries
         self.entries = entries
         super().__init__(REUC_EXTENSION, b"")
         super().__init__(REUC_EXTENSION, b"")
 
 
     @classmethod
     @classmethod
     def from_bytes(cls, data: bytes) -> "ResolveUndoExtension":
     def from_bytes(cls, data: bytes) -> "ResolveUndoExtension":
+        """Parse ResolveUndoExtension from bytes.
+
+        Args:
+          data: Raw bytes to parse
+
+        Returns:
+          ResolveUndoExtension instance
+        """
         # TODO: Implement resolve undo parsing
         # TODO: Implement resolve undo parsing
         return cls([])
         return cls([])
 
 
     def to_bytes(self) -> bytes:
     def to_bytes(self) -> bytes:
+        """Serialize ResolveUndoExtension to bytes.
+
+        Returns:
+          Serialized extension data
+        """
         # TODO: Implement resolve undo serialization
         # TODO: Implement resolve undo serialization
         return b""
         return b""
 
 
@@ -354,15 +403,34 @@ class UntrackedExtension(IndexExtension):
     """Untracked cache extension."""
     """Untracked cache extension."""
 
 
     def __init__(self, data: bytes) -> None:
     def __init__(self, data: bytes) -> None:
+        """Initialize UntrackedExtension.
+
+        Args:
+            data: Raw untracked cache data
+        """
         super().__init__(UNTR_EXTENSION, data)
         super().__init__(UNTR_EXTENSION, data)
 
 
     @classmethod
     @classmethod
     def from_bytes(cls, data: bytes) -> "UntrackedExtension":
     def from_bytes(cls, data: bytes) -> "UntrackedExtension":
+        """Parse UntrackedExtension from bytes.
+
+        Args:
+          data: Raw bytes to parse
+
+        Returns:
+          UntrackedExtension instance
+        """
         return cls(data)
         return cls(data)
 
 
 
 
 @dataclass
 @dataclass
 class IndexEntry:
 class IndexEntry:
+    """Represents an entry in the Git index.
+
+    This is a higher-level representation of an index entry that includes
+    parsed data and convenience methods.
+    """
+
     ctime: Union[int, float, tuple[int, int]]
     ctime: Union[int, float, tuple[int, int]]
     mtime: Union[int, float, tuple[int, int]]
     mtime: Union[int, float, tuple[int, int]]
     dev: int
     dev: int
@@ -377,6 +445,14 @@ class IndexEntry:
 
 
     @classmethod
     @classmethod
     def from_serialized(cls, serialized: SerializedIndexEntry) -> "IndexEntry":
     def from_serialized(cls, serialized: SerializedIndexEntry) -> "IndexEntry":
+        """Create an IndexEntry from a SerializedIndexEntry.
+
+        Args:
+          serialized: SerializedIndexEntry to convert
+
+        Returns:
+          New IndexEntry instance
+        """
         return cls(
         return cls(
             ctime=serialized.ctime,
             ctime=serialized.ctime,
             mtime=serialized.mtime,
             mtime=serialized.mtime,
@@ -392,6 +468,15 @@ class IndexEntry:
         )
         )
 
 
     def serialize(self, name: bytes, stage: Stage) -> SerializedIndexEntry:
     def serialize(self, name: bytes, stage: Stage) -> SerializedIndexEntry:
+        """Serialize this entry with a given name and stage.
+
+        Args:
+          name: Path name for the entry
+          stage: Merge conflict stage
+
+        Returns:
+          SerializedIndexEntry ready for writing to disk
+        """
         # Clear out any existing stage bits, then set them from the Stage.
         # Clear out any existing stage bits, then set them from the Stage.
         new_flags = self.flags & ~FLAG_STAGEMASK
         new_flags = self.flags & ~FLAG_STAGEMASK
         new_flags |= stage.value << FLAG_STAGESHIFT
         new_flags |= stage.value << FLAG_STAGESHIFT
@@ -411,6 +496,11 @@ class IndexEntry:
         )
         )
 
 
     def stage(self) -> Stage:
     def stage(self) -> Stage:
+        """Get the merge conflict stage of this entry.
+
+        Returns:
+          Stage enum value
+        """
         return Stage((self.flags & FLAG_STAGEMASK) >> FLAG_STAGESHIFT)
         return Stage((self.flags & FLAG_STAGEMASK) >> FLAG_STAGESHIFT)
 
 
     @property
     @property
@@ -420,6 +510,7 @@ class IndexEntry:
 
 
     def set_skip_worktree(self, skip: bool = True) -> None:
     def set_skip_worktree(self, skip: bool = True) -> None:
         """Helper method to set or clear the skip-worktree bit in extended_flags.
         """Helper method to set or clear the skip-worktree bit in extended_flags.
+
         Also sets FLAG_EXTENDED in self.flags if needed.
         Also sets FLAG_EXTENDED in self.flags if needed.
         """
         """
         if skip:
         if skip:
@@ -448,6 +539,13 @@ class ConflictedIndexEntry:
         this: Optional[IndexEntry] = None,
         this: Optional[IndexEntry] = None,
         other: Optional[IndexEntry] = None,
         other: Optional[IndexEntry] = None,
     ) -> None:
     ) -> None:
+        """Initialize ConflictedIndexEntry.
+
+        Args:
+            ancestor: The common ancestor entry
+            this: The current branch entry
+            other: The other branch entry
+        """
         self.ancestor = ancestor
         self.ancestor = ancestor
         self.this = this
         self.this = this
         self.other = other
         self.other = other
@@ -624,6 +722,11 @@ class UnsupportedIndexFormat(Exception):
     """An unsupported index format was encountered."""
     """An unsupported index format was encountered."""
 
 
     def __init__(self, version: int) -> None:
     def __init__(self, version: int) -> None:
+        """Initialize UnsupportedIndexFormat exception.
+
+        Args:
+            version: The unsupported index format version
+        """
         self.index_format_version = version
         self.index_format_version = version
 
 
 
 
@@ -740,6 +843,7 @@ def read_index_dict(
     f: BinaryIO,
     f: BinaryIO,
 ) -> dict[bytes, Union[IndexEntry, ConflictedIndexEntry]]:
 ) -> dict[bytes, Union[IndexEntry, ConflictedIndexEntry]]:
     """Read an index file and return it as a dictionary.
     """Read an index file and return it as a dictionary.
+
        Dict Key is tuple of path and stage number, as
        Dict Key is tuple of path and stage number, as
             path alone is not unique
             path alone is not unique
     Args:
     Args:
@@ -811,6 +915,7 @@ def write_index_dict(
     extensions: Optional[list[IndexExtension]] = None,
     extensions: Optional[list[IndexExtension]] = None,
 ) -> None:
 ) -> None:
     """Write an index file based on the contents of a dictionary.
     """Write an index file based on the contents of a dictionary.
+
     being careful to sort by path and then by stage.
     being careful to sort by path and then by stage.
     """
     """
     entries_list = []
     entries_list = []
@@ -889,9 +994,15 @@ class Index:
 
 
     @property
     @property
     def path(self) -> Union[bytes, str]:
     def path(self) -> Union[bytes, str]:
+        """Get the path to the index file.
+
+        Returns:
+          Path to the index file
+        """
         return self._filename
         return self._filename
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of Index."""
         return f"{self.__class__.__name__}({self._filename!r})"
         return f"{self.__class__.__name__}({self._filename!r})"
 
 
     def write(self) -> None:
     def write(self) -> None:
@@ -967,6 +1078,7 @@ class Index:
         return iter(self._byname)
         return iter(self._byname)
 
 
     def __contains__(self, key: bytes) -> bool:
     def __contains__(self, key: bytes) -> bool:
+        """Check if a path exists in the index."""
         return key in self._byname
         return key in self._byname
 
 
     def get_sha1(self, path: bytes) -> bytes:
     def get_sha1(self, path: bytes) -> bytes:
@@ -992,6 +1104,11 @@ class Index:
             yield path, entry.sha, cleanup_mode(entry.mode)
             yield path, entry.sha, cleanup_mode(entry.mode)
 
 
     def has_conflicts(self) -> bool:
     def has_conflicts(self) -> bool:
+        """Check if the index contains any conflicted entries.
+
+        Returns:
+          True if any entries are conflicted, False otherwise
+        """
         for value in self._byname.values():
         for value in self._byname.values():
             if isinstance(value, ConflictedIndexEntry):
             if isinstance(value, ConflictedIndexEntry):
                 return True
                 return True
@@ -1004,27 +1121,49 @@ class Index:
     def __setitem__(
     def __setitem__(
         self, name: bytes, value: Union[IndexEntry, ConflictedIndexEntry]
         self, name: bytes, value: Union[IndexEntry, ConflictedIndexEntry]
     ) -> None:
     ) -> None:
+        """Set an entry in the index."""
         assert isinstance(name, bytes)
         assert isinstance(name, bytes)
         self._byname[name] = value
         self._byname[name] = value
 
 
     def __delitem__(self, name: bytes) -> None:
     def __delitem__(self, name: bytes) -> None:
+        """Delete an entry from the index."""
         del self._byname[name]
         del self._byname[name]
 
 
     def iteritems(
     def iteritems(
         self,
         self,
     ) -> Iterator[tuple[bytes, Union[IndexEntry, ConflictedIndexEntry]]]:
     ) -> Iterator[tuple[bytes, Union[IndexEntry, ConflictedIndexEntry]]]:
+        """Iterate over (path, entry) pairs in the index.
+
+        Returns:
+          Iterator of (path, entry) tuples
+        """
         return iter(self._byname.items())
         return iter(self._byname.items())
 
 
     def items(self) -> Iterator[tuple[bytes, Union[IndexEntry, ConflictedIndexEntry]]]:
     def items(self) -> Iterator[tuple[bytes, Union[IndexEntry, ConflictedIndexEntry]]]:
+        """Get an iterator over (path, entry) pairs.
+
+        Returns:
+          Iterator of (path, entry) tuples
+        """
         return iter(self._byname.items())
         return iter(self._byname.items())
 
 
     def update(
     def update(
         self, entries: dict[bytes, Union[IndexEntry, ConflictedIndexEntry]]
         self, entries: dict[bytes, Union[IndexEntry, ConflictedIndexEntry]]
     ) -> None:
     ) -> None:
+        """Update the index with multiple entries.
+
+        Args:
+          entries: Dictionary mapping paths to index entries
+        """
         for key, value in entries.items():
         for key, value in entries.items():
             self[key] = value
             self[key] = value
 
 
     def paths(self) -> Generator[bytes, None, None]:
     def paths(self) -> Generator[bytes, None, None]:
+        """Generate all paths in the index.
+
+        Yields:
+          Path names as bytes
+        """
         yield from self._byname.keys()
         yield from self._byname.keys()
 
 
     def changes_from_tree(
     def changes_from_tree(
@@ -1147,8 +1286,7 @@ def changes_from_tree(
         tuple[Optional[bytes], Optional[bytes]],
         tuple[Optional[bytes], Optional[bytes]],
     ]
     ]
 ]:
 ]:
-    """Find the differences between the contents of a tree and
-    a working copy.
+    """Find the differences between the contents of a tree and a working copy.
 
 
     Args:
     Args:
       names: Iterable of names in the working copy
       names: Iterable of names in the working copy
@@ -1194,6 +1332,7 @@ def index_entry_from_stat(
     Args:
     Args:
       stat_val: POSIX stat_result instance
       stat_val: POSIX stat_result instance
       hex_sha: Hex sha of the object
       hex_sha: Hex sha of the object
+      mode: Optional file mode, will be derived from stat if not provided
     """
     """
     if mode is None:
     if mode is None:
         mode = cleanup_mode(stat_val.st_mode)
         mode = cleanup_mode(stat_val.st_mode)
@@ -1221,7 +1360,14 @@ if sys.platform == "win32":
     # https://github.com/jelmer/dulwich/issues/1005
     # https://github.com/jelmer/dulwich/issues/1005
 
 
     class WindowsSymlinkPermissionError(PermissionError):
     class WindowsSymlinkPermissionError(PermissionError):
+        """Windows-specific error for symlink creation failures.
+
+        This error is raised when symlink creation fails on Windows,
+        typically due to lack of developer mode or administrator privileges.
+        """
+
         def __init__(self, errno: int, msg: str, filename: Optional[str]) -> None:
         def __init__(self, errno: int, msg: str, filename: Optional[str]) -> None:
+            """Initialize WindowsSymlinkPermissionError."""
             super(PermissionError, self).__init__(
             super(PermissionError, self).__init__(
                 errno,
                 errno,
                 f"Unable to create symlink; do you have developer mode enabled? {msg}",
                 f"Unable to create symlink; do you have developer mode enabled? {msg}",
@@ -1235,6 +1381,17 @@ if sys.platform == "win32":
         *,
         *,
         dir_fd: Optional[int] = None,
         dir_fd: Optional[int] = None,
     ) -> None:
     ) -> None:
+        """Create a symbolic link on Windows with better error handling.
+
+        Args:
+          src: Source path for the symlink
+          dst: Destination path where symlink will be created
+          target_is_directory: Whether the target is a directory
+          dir_fd: Optional directory file descriptor
+
+        Raises:
+          WindowsSymlinkPermissionError: If symlink creation fails due to permissions
+        """
         try:
         try:
             return os.symlink(
             return os.symlink(
                 src, dst, target_is_directory=target_is_directory, dir_fd=dir_fd
                 src, dst, target_is_directory=target_is_directory, dir_fd=dir_fd
@@ -1264,6 +1421,7 @@ def build_file_from_blob(
       target_path: Path to write to
       target_path: Path to write to
       honor_filemode: An optional flag to honor core.filemode setting in
       honor_filemode: An optional flag to honor core.filemode setting in
         config file, default is core.filemode=True, change executable bit
         config file, default is core.filemode=True, change executable bit
+      tree_encoding: Encoding to use for tree contents
       symlink_fn: Function to use for creating symlinks
       symlink_fn: Function to use for creating symlinks
     Returns: stat object for the file
     Returns: stat object for the file
     """
     """
@@ -1346,10 +1504,26 @@ def get_path_element_normalizer(config: "Config") -> Callable[[bytes], bytes]:
 
 
 
 
 def validate_path_element_default(element: bytes) -> bool:
 def validate_path_element_default(element: bytes) -> bool:
+    """Validate a path element using default rules.
+
+    Args:
+      element: Path element to validate
+
+    Returns:
+      True if path element is valid, False otherwise
+    """
     return _normalize_path_element_default(element) not in INVALID_DOTNAMES
     return _normalize_path_element_default(element) not in INVALID_DOTNAMES
 
 
 
 
 def validate_path_element_ntfs(element: bytes) -> bool:
 def validate_path_element_ntfs(element: bytes) -> bool:
+    """Validate a path element using NTFS filesystem rules.
+
+    Args:
+      element: Path element to validate
+
+    Returns:
+      True if path element is valid for NTFS, False otherwise
+    """
     normalized = _normalize_path_element_ntfs(element)
     normalized = _normalize_path_element_ntfs(element)
     if normalized in INVALID_DOTNAMES:
     if normalized in INVALID_DOTNAMES:
         return False
         return False
@@ -1437,6 +1611,7 @@ def build_index_from_tree(
         config file, default is core.filemode=True, change executable bit
         config file, default is core.filemode=True, change executable bit
       validate_path_element: Function to validate path elements to check
       validate_path_element: Function to validate path elements to check
         out; default just refuses .git and .. directories.
         out; default just refuses .git and .. directories.
+      symlink_fn: Function to use for creating symlinks
       blob_normalizer: An optional BlobNormalizer to use for converting line
       blob_normalizer: An optional BlobNormalizer to use for converting line
         endings when writing blobs to the working directory.
         endings when writing blobs to the working directory.
       tree_encoding: Encoding used for tree paths (default: utf-8)
       tree_encoding: Encoding used for tree paths (default: utf-8)
@@ -1510,6 +1685,7 @@ def blob_from_path_and_mode(
     Args:
     Args:
       fs_path: Full file system path to file
       fs_path: Full file system path to file
       mode: File mode
       mode: File mode
+      tree_encoding: Encoding to use for tree contents
     Returns: A `Blob` object
     Returns: A `Blob` object
     """
     """
     assert isinstance(fs_path, bytes)
     assert isinstance(fs_path, bytes)
@@ -1534,6 +1710,7 @@ def blob_from_path_and_stat(
     Args:
     Args:
       fs_path: Full file system path to file
       fs_path: Full file system path to file
       st: A stat object
       st: A stat object
+      tree_encoding: Encoding to use for tree contents
     Returns: A `Blob` object
     Returns: A `Blob` object
     """
     """
     return blob_from_path_and_mode(fs_path, st.st_mode, tree_encoding)
     return blob_from_path_and_mode(fs_path, st.st_mode, tree_encoding)
@@ -2269,6 +2446,7 @@ def get_unstaged_changes(
     Args:
     Args:
       index: index to check
       index: index to check
       root_path: path in which to find files
       root_path: path in which to find files
+      filter_blob_callback: Optional callback to filter blobs
     Returns: iterator over paths with unstaged changes
     Returns: iterator over paths with unstaged changes
     """
     """
     # For each entry in the index check the sha1 & ensure not staged
     # For each entry in the index check the sha1 & ensure not staged
@@ -2368,6 +2546,17 @@ def _fs_to_tree_path(fs_path: Union[str, bytes], tree_encoding: str = "utf-8") -
 
 
 
 
 def index_entry_from_directory(st: os.stat_result, path: bytes) -> Optional[IndexEntry]:
 def index_entry_from_directory(st: os.stat_result, path: bytes) -> Optional[IndexEntry]:
+    """Create an index entry for a directory.
+
+    This is only used for submodules (directories containing .git).
+
+    Args:
+      st: Stat result for the directory
+      path: Path to the directory
+
+    Returns:
+      IndexEntry for a submodule, or None if not a submodule
+    """
     if os.path.exists(os.path.join(path, b".git")):
     if os.path.exists(os.path.join(path, b".git")):
         head = read_submodule_head(path)
         head = read_submodule_head(path)
         if head is None:
         if head is None:
@@ -2436,6 +2625,7 @@ def iter_fresh_objects(
     """Iterate over versions of objects on disk referenced by index.
     """Iterate over versions of objects on disk referenced by index.
 
 
     Args:
     Args:
+      paths: Paths to check
       root_path: Root path to access from
       root_path: Root path to access from
       include_deleted: Include deleted entries with sha and
       include_deleted: Include deleted entries with sha and
         mode set to None
         mode set to None
@@ -2473,9 +2663,11 @@ class locked_index:
     _file: "_GitFile"
     _file: "_GitFile"
 
 
     def __init__(self, path: Union[bytes, str]) -> None:
     def __init__(self, path: Union[bytes, str]) -> None:
+        """Initialize locked_index."""
         self._path = path
         self._path = path
 
 
     def __enter__(self) -> Index:
     def __enter__(self) -> Index:
+        """Enter context manager and lock index."""
         f = GitFile(self._path, "wb")
         f = GitFile(self._path, "wb")
         assert isinstance(f, _GitFile)  # GitFile in write mode always returns _GitFile
         assert isinstance(f, _GitFile)  # GitFile in write mode always returns _GitFile
         self._file = f
         self._file = f
@@ -2488,6 +2680,7 @@ class locked_index:
         exc_value: Optional[BaseException],
         exc_value: Optional[BaseException],
         traceback: Optional[types.TracebackType],
         traceback: Optional[types.TracebackType],
     ) -> None:
     ) -> None:
+        """Exit context manager and unlock index."""
         if exc_type is not None:
         if exc_type is not None:
             self._file.abort()
             self._file.abort()
             return
             return

+ 6 - 0
dulwich/lfs.py

@@ -91,10 +91,12 @@ class LFSStore:
     """Stores objects on disk, indexed by SHA256."""
     """Stores objects on disk, indexed by SHA256."""
 
 
     def __init__(self, path: str) -> None:
     def __init__(self, path: str) -> None:
+        """Initialize LFSStore."""
         self.path = path
         self.path = path
 
 
     @classmethod
     @classmethod
     def create(cls, lfs_dir: str) -> "LFSStore":
     def create(cls, lfs_dir: str) -> "LFSStore":
+        """Create a new LFS store."""
         if not os.path.isdir(lfs_dir):
         if not os.path.isdir(lfs_dir):
             os.mkdir(lfs_dir)
             os.mkdir(lfs_dir)
         tmp_dir = os.path.join(lfs_dir, "tmp")
         tmp_dir = os.path.join(lfs_dir, "tmp")
@@ -107,6 +109,7 @@ class LFSStore:
 
 
     @classmethod
     @classmethod
     def from_repo(cls, repo: "Repo", create: bool = False) -> "LFSStore":
     def from_repo(cls, repo: "Repo", create: bool = False) -> "LFSStore":
+        """Create LFS store from repository."""
         lfs_dir = os.path.join(repo.controldir(), "lfs")
         lfs_dir = os.path.join(repo.controldir(), "lfs")
         if create:
         if create:
             return cls.create(lfs_dir)
             return cls.create(lfs_dir)
@@ -114,6 +117,7 @@ class LFSStore:
 
 
     @classmethod
     @classmethod
     def from_controldir(cls, controldir: str, create: bool = False) -> "LFSStore":
     def from_controldir(cls, controldir: str, create: bool = False) -> "LFSStore":
+        """Create LFS store from control directory."""
         lfs_dir = os.path.join(controldir, "lfs")
         lfs_dir = os.path.join(controldir, "lfs")
         if create:
         if create:
             return cls.create(lfs_dir)
             return cls.create(lfs_dir)
@@ -158,6 +162,7 @@ class LFSPointer:
     """Represents an LFS pointer file."""
     """Represents an LFS pointer file."""
 
 
     def __init__(self, oid: str, size: int) -> None:
     def __init__(self, oid: str, size: int) -> None:
+        """Initialize LFSPointer."""
         self.oid = oid
         self.oid = oid
         self.size = size
         self.size = size
 
 
@@ -226,6 +231,7 @@ class LFSFilterDriver:
     def __init__(
     def __init__(
         self, lfs_store: "LFSStore", config: Optional["Config"] = None
         self, lfs_store: "LFSStore", config: Optional["Config"] = None
     ) -> None:
     ) -> None:
+        """Initialize LFSFilterDriver."""
         self.lfs_store = lfs_store
         self.lfs_store = lfs_store
         self.config = config
         self.config = config
 
 

+ 7 - 0
dulwich/lfs_server.py

@@ -246,6 +246,13 @@ class LFSServer(HTTPServer):
         lfs_store: LFSStore,
         lfs_store: LFSStore,
         log_requests: bool = False,
         log_requests: bool = False,
     ) -> None:
     ) -> None:
+        """Initialize LFSServer.
+
+        Args:
+          server_address: Tuple of (host, port) to bind to
+          lfs_store: LFS store instance to use
+          log_requests: Whether to log incoming requests
+        """
         super().__init__(server_address, LFSRequestHandler)
         super().__init__(server_address, LFSRequestHandler)
         self.lfs_store = lfs_store
         self.lfs_store = lfs_store
         self.log_requests = log_requests
         self.log_requests = log_requests

+ 8 - 6
dulwich/line_ending.py

@@ -163,6 +163,7 @@ class LineEndingFilter(FilterDriver):
         smudge_conversion: Optional[Callable[[bytes], bytes]] = None,
         smudge_conversion: Optional[Callable[[bytes], bytes]] = None,
         binary_detection: bool = True,
         binary_detection: bool = True,
     ):
     ):
+        """Initialize LineEndingFilter."""
         self.clean_conversion = clean_conversion
         self.clean_conversion = clean_conversion
         self.smudge_conversion = smudge_conversion
         self.smudge_conversion = smudge_conversion
         self.binary_detection = binary_detection
         self.binary_detection = binary_detection
@@ -323,8 +324,7 @@ def get_checkin_filter_autocrlf(
 
 
 
 
 class BlobNormalizer(FilterBlobNormalizer):
 class BlobNormalizer(FilterBlobNormalizer):
-    """An object to store computation result of which filter to apply based
-    on configuration, gitattributes, path and operation (checkin or checkout).
+    """An object to store computation result of which filter to apply based on configuration, gitattributes, path and operation (checkin or checkout).
 
 
     This class maintains backward compatibility while using the filter infrastructure.
     This class maintains backward compatibility while using the filter infrastructure.
     """
     """
@@ -336,6 +336,7 @@ class BlobNormalizer(FilterBlobNormalizer):
         core_eol: str = "native",
         core_eol: str = "native",
         autocrlf: bytes = b"false",
         autocrlf: bytes = b"false",
     ) -> None:
     ) -> None:
+        """Initialize FilteringBlobNormalizer."""
         # Set up a filter registry with line ending filters
         # Set up a filter registry with line ending filters
         filter_registry = FilterRegistry(config_stack)
         filter_registry = FilterRegistry(config_stack)
 
 
@@ -432,10 +433,7 @@ class BlobNormalizer(FilterBlobNormalizer):
 def normalize_blob(
 def normalize_blob(
     blob: Blob, conversion: Callable[[bytes], bytes], binary_detection: bool
     blob: Blob, conversion: Callable[[bytes], bytes], binary_detection: bool
 ) -> Blob:
 ) -> Blob:
-    """Takes a blob as input returns either the original blob if
-    binary_detection is True and the blob content looks like binary, else
-    return a new blob with converted data.
-    """
+    """Normalize blob by applying line ending conversion."""
     # Read the original blob
     # Read the original blob
     data = blob.data
     data = blob.data
 
 
@@ -456,6 +454,8 @@ def normalize_blob(
 
 
 
 
 class TreeBlobNormalizer(BlobNormalizer):
 class TreeBlobNormalizer(BlobNormalizer):
+    """Blob normalizer that tracks existing files in a tree."""
+
     def __init__(
     def __init__(
         self,
         self,
         config_stack: "StackedConfig",
         config_stack: "StackedConfig",
@@ -465,6 +465,7 @@ class TreeBlobNormalizer(BlobNormalizer):
         core_eol: str = "native",
         core_eol: str = "native",
         autocrlf: bytes = b"false",
         autocrlf: bytes = b"false",
     ) -> None:
     ) -> None:
+        """Initialize TreeBlobNormalizer."""
         super().__init__(config_stack, git_attributes, core_eol, autocrlf)
         super().__init__(config_stack, git_attributes, core_eol, autocrlf)
         if tree:
         if tree:
             self.existing_paths = {
             self.existing_paths = {
@@ -474,6 +475,7 @@ class TreeBlobNormalizer(BlobNormalizer):
             self.existing_paths = set()
             self.existing_paths = set()
 
 
     def checkin_normalize(self, blob: Blob, tree_path: bytes) -> Blob:
     def checkin_normalize(self, blob: Blob, tree_path: bytes) -> Blob:
+        """Normalize blob for checkin, considering existing tree state."""
         # Existing files should only be normalized on checkin if it was
         # Existing files should only be normalized on checkin if it was
         # previously normalized on checkout
         # previously normalized on checkout
         if (
         if (

+ 18 - 0
dulwich/lru_cache.py

@@ -78,6 +78,12 @@ class LRUCache(Generic[K, V]):
     def __init__(
     def __init__(
         self, max_cache: int = 100, after_cleanup_count: Optional[int] = None
         self, max_cache: int = 100, after_cleanup_count: Optional[int] = None
     ) -> None:
     ) -> None:
+        """Initialize LRUCache.
+
+        Args:
+          max_cache: Maximum number of entries to cache
+          after_cleanup_count: Number of entries to keep after cleanup
+        """
         self._cache: dict[K, _LRUNode[K, V]] = {}
         self._cache: dict[K, _LRUNode[K, V]] = {}
         # The "HEAD" of the lru linked list
         # The "HEAD" of the lru linked list
         self._most_recently_used = None
         self._most_recently_used = None
@@ -86,9 +92,11 @@ class LRUCache(Generic[K, V]):
         self._update_max_cache(max_cache, after_cleanup_count)
         self._update_max_cache(max_cache, after_cleanup_count)
 
 
     def __contains__(self, key: K) -> bool:
     def __contains__(self, key: K) -> bool:
+        """Check if key is in cache."""
         return key in self._cache
         return key in self._cache
 
 
     def __getitem__(self, key: K) -> V:
     def __getitem__(self, key: K) -> V:
+        """Get item from cache and mark as recently used."""
         cache = self._cache
         cache = self._cache
         node = cache[key]
         node = cache[key]
         # Inlined from _record_access to decrease the overhead of __getitem__
         # Inlined from _record_access to decrease the overhead of __getitem__
@@ -122,6 +130,7 @@ class LRUCache(Generic[K, V]):
         return node.value
         return node.value
 
 
     def __len__(self) -> int:
     def __len__(self) -> int:
+        """Return number of items in cache."""
         return len(self._cache)
         return len(self._cache)
 
 
     def _walk_lru(self) -> Iterator[_LRUNode[K, V]]:
     def _walk_lru(self) -> Iterator[_LRUNode[K, V]]:
@@ -196,6 +205,15 @@ class LRUCache(Generic[K, V]):
         return self._max_cache
         return self._max_cache
 
 
     def get(self, key: K, default: Optional[V] = None) -> Optional[V]:
     def get(self, key: K, default: Optional[V] = None) -> Optional[V]:
+        """Get value from cache with default if not found.
+
+        Args:
+          key: Key to look up
+          default: Default value if key not found
+
+        Returns:
+          Value from cache or default
+        """
         node = self._cache.get(key, None)
         node = self._cache.get(key, None)
         if node is None:
         if node is None:
             return default
             return default

+ 21 - 0
dulwich/mailmap.py

@@ -26,6 +26,14 @@ from typing import IO, Optional, Union
 
 
 
 
 def parse_identity(text: bytes) -> tuple[Optional[bytes], Optional[bytes]]:
 def parse_identity(text: bytes) -> tuple[Optional[bytes], Optional[bytes]]:
+    """Parse an identity string into name and email.
+
+    Args:
+      text: Identity string in format "Name <email>"
+
+    Returns:
+      Tuple of (name, email) where either can be None
+    """
     # TODO(jelmer): Integrate this with dulwich.fastexport.split_email and
     # TODO(jelmer): Integrate this with dulwich.fastexport.split_email and
     # dulwich.repo.check_user_identity
     # dulwich.repo.check_user_identity
     (name_str, email_str) = text.rsplit(b"<", 1)
     (name_str, email_str) = text.rsplit(b"<", 1)
@@ -81,6 +89,11 @@ class Mailmap:
             ]
             ]
         ] = None,
         ] = None,
     ) -> None:
     ) -> None:
+        """Initialize Mailmap.
+
+        Args:
+          map: Optional iterator of (canonical_identity, from_identity) tuples
+        """
         self._table: dict[
         self._table: dict[
             tuple[Optional[bytes], Optional[bytes]],
             tuple[Optional[bytes], Optional[bytes]],
             tuple[Optional[bytes], Optional[bytes]],
             tuple[Optional[bytes], Optional[bytes]],
@@ -143,5 +156,13 @@ class Mailmap:
 
 
     @classmethod
     @classmethod
     def from_path(cls, path: str) -> "Mailmap":
     def from_path(cls, path: str) -> "Mailmap":
+        """Create Mailmap from file path.
+
+        Args:
+          path: Path to mailmap file
+
+        Returns:
+          Mailmap instance
+        """
         with open(path, "rb") as f:
         with open(path, "rb") as f:
             return cls(read_mailmap(f))
             return cls(read_mailmap(f))

+ 6 - 0
dulwich/merge.py

@@ -18,6 +18,12 @@ class MergeConflict(Exception):
     """Raised when a merge conflict occurs."""
     """Raised when a merge conflict occurs."""
 
 
     def __init__(self, path: bytes, message: str) -> None:
     def __init__(self, path: bytes, message: str) -> None:
+        """Initialize MergeConflict.
+
+        Args:
+          path: Path to the conflicted file
+          message: Conflict description
+        """
         self.path = path
         self.path = path
         super().__init__(f"Merge conflict in {path!r}: {message}")
         super().__init__(f"Merge conflict in {path!r}: {message}")
 
 

+ 47 - 0
dulwich/notes.py

@@ -52,6 +52,15 @@ def get_note_fanout_level(tree: Tree, object_store: "BaseObjectStore") -> int:
 
 
     # Count the total number of notes in the tree recursively
     # Count the total number of notes in the tree recursively
     def count_notes(tree: Tree, level: int = 0) -> int:
     def count_notes(tree: Tree, level: int = 0) -> int:
+        """Count notes in a tree recursively.
+
+        Args:
+            tree: Tree to count notes in
+            level: Current recursion level
+
+        Returns:
+            Total number of notes
+        """
         count = 0
         count = 0
         for name, mode, sha in tree.items():
         for name, mode, sha in tree.items():
             if stat.S_ISREG(mode):
             if stat.S_ISREG(mode):
@@ -223,6 +232,16 @@ class NotesTree:
 
 
             # Build new tree structure
             # Build new tree structure
             def update_tree(tree: Tree, components: list, blob_sha: bytes) -> Tree:
             def update_tree(tree: Tree, components: list, blob_sha: bytes) -> Tree:
+                """Update tree with new note entry.
+
+                Args:
+                    tree: Tree to update
+                    components: Path components
+                    blob_sha: SHA of the note blob
+
+                Returns:
+                    Updated tree
+                """
                 if len(components) == 1:
                 if len(components) == 1:
                     # Leaf level - add the note blob
                     # Leaf level - add the note blob
                     new_tree = Tree()
                     new_tree = Tree()
@@ -368,6 +387,16 @@ class NotesTree:
 
 
         # Build new tree structure
         # Build new tree structure
         def update_tree(tree: Tree, components: list, blob_sha: bytes) -> Tree:
         def update_tree(tree: Tree, components: list, blob_sha: bytes) -> Tree:
+            """Update tree with new note entry.
+
+            Args:
+                tree: Tree to update
+                components: Path components
+                blob_sha: SHA of the note blob
+
+            Returns:
+                Updated tree
+            """
             if len(components) == 1:
             if len(components) == 1:
                 # Leaf level - add the note blob
                 # Leaf level - add the note blob
                 new_tree = Tree()
                 new_tree = Tree()
@@ -429,6 +458,15 @@ class NotesTree:
 
 
         # Build new tree structure without the note
         # Build new tree structure without the note
         def remove_from_tree(tree: Tree, components: list) -> Optional[Tree]:
         def remove_from_tree(tree: Tree, components: list) -> Optional[Tree]:
+            """Remove note entry from tree.
+
+            Args:
+                tree: Tree to remove from
+                components: Path components
+
+            Returns:
+                Updated tree or None if empty
+            """
             if len(components) == 1:
             if len(components) == 1:
                 # Leaf level - remove the note
                 # Leaf level - remove the note
                 new_tree = Tree()
                 new_tree = Tree()
@@ -484,6 +522,15 @@ class NotesTree:
         """
         """
 
 
         def walk_tree(tree: Tree, prefix: bytes = b"") -> Iterator[tuple[bytes, bytes]]:
         def walk_tree(tree: Tree, prefix: bytes = b"") -> Iterator[tuple[bytes, bytes]]:
+            """Walk the notes tree recursively.
+
+            Args:
+                tree: Tree to walk
+                prefix: Path prefix for current level
+
+            Yields:
+                Tuples of (object_sha, note_sha)
+            """
             for name, mode, sha in tree.items():
             for name, mode, sha in tree.items():
                 if stat.S_ISDIR(mode):  # Directory
                 if stat.S_ISDIR(mode):  # Directory
                     subtree = self._object_store[sha]
                     subtree = self._object_store[sha]

+ 21 - 0
dulwich/object_store.py

@@ -156,10 +156,12 @@ def get_depth(
     max_depth=None,
     max_depth=None,
 ):
 ):
     """Return the current available depth for the given head.
     """Return the current available depth for the given head.
+
     For commits with multiple parents, the largest possible depth will be
     For commits with multiple parents, the largest possible depth will be
     returned.
     returned.
 
 
     Args:
     Args:
+        store: Object store to search in
         head: commit to start from
         head: commit to start from
         get_parents: optional function for getting the parents of a commit
         get_parents: optional function for getting the parents of a commit
         max_depth: maximum depth to search
         max_depth: maximum depth to search
@@ -272,6 +274,7 @@ class BaseObjectStore:
 
 
         Args:
         Args:
           objects: Iterable over a list of (object, path) tuples
           objects: Iterable over a list of (object, path) tuples
+          progress: Optional progress callback
         """
         """
         raise NotImplementedError(self.add_objects)
         raise NotImplementedError(self.add_objects)
 
 
@@ -455,6 +458,7 @@ class BaseObjectStore:
         max_depth=None,
         max_depth=None,
     ):
     ):
         """Return the current available depth for the given head.
         """Return the current available depth for the given head.
+
         For commits with multiple parents, the largest possible depth will be
         For commits with multiple parents, the largest possible depth will be
         returned.
         returned.
 
 
@@ -583,6 +587,8 @@ class PackBasedObjectStore(BaseObjectStore, PackedObjectContainer):
 
 
         Args:
         Args:
           count: Number of items to add
           count: Number of items to add
+          unpacked_objects: Iterator of UnpackedObject instances
+          progress: Optional progress callback
         """
         """
         if count == 0:
         if count == 0:
             # Don't bother writing an empty pack file
             # Don't bother writing an empty pack file
@@ -950,6 +956,7 @@ class PackBasedObjectStore(BaseObjectStore, PackedObjectContainer):
 
 
         Args:
         Args:
           sha1: sha for the object.
           sha1: sha for the object.
+          include_comp: Whether to include compression metadata.
         """
         """
         if sha1 == ZERO_SHA:
         if sha1 == ZERO_SHA:
             raise KeyError(sha1)
             raise KeyError(sha1)
@@ -992,6 +999,7 @@ class PackBasedObjectStore(BaseObjectStore, PackedObjectContainer):
         Args:
         Args:
           objects: Iterable over (object, path) tuples, should support
           objects: Iterable over (object, path) tuples, should support
             __len__.
             __len__.
+          progress: Optional progress reporting function.
         Returns: Pack object of the objects written.
         Returns: Pack object of the objects written.
         """
         """
         count = len(objects)
         count = len(objects)
@@ -1351,7 +1359,9 @@ class DiskObjectStore(PackBasedObjectStore):
         Args:
         Args:
           f: Open file object for the pack.
           f: Open file object for the pack.
           path: Path to the pack file.
           path: Path to the pack file.
+          num_objects: Number of objects in the pack.
           indexer: A PackIndexer for indexing the pack.
           indexer: A PackIndexer for indexing the pack.
+          progress: Optional progress reporting function.
         """
         """
         entries = []
         entries = []
         for i, entry in enumerate(indexer):
         for i, entry in enumerate(indexer):
@@ -1426,6 +1436,7 @@ class DiskObjectStore(PackBasedObjectStore):
             requested bytes are read.
             requested bytes are read.
           read_some: Read function that returns at least one byte, but may
           read_some: Read function that returns at least one byte, but may
             not return the number of bytes requested.
             not return the number of bytes requested.
+          progress: Optional progress reporting function.
         Returns: A Pack object pointing at the now-completed thin pack in the
         Returns: A Pack object pointing at the now-completed thin pack in the
             objects/pack directory.
             objects/pack directory.
         """
         """
@@ -1765,6 +1776,7 @@ class MemoryObjectStore(BaseObjectStore):
 
 
         Args:
         Args:
           objects: Iterable over a list of (object, path) tuples
           objects: Iterable over a list of (object, path) tuples
+          progress: Optional progress reporting function.
         """
         """
         for obj, path in objects:
         for obj, path in objects:
             self.add_object(obj)
             self.add_object(obj)
@@ -1806,6 +1818,8 @@ class MemoryObjectStore(BaseObjectStore):
 
 
         Args:
         Args:
           count: Number of items to add
           count: Number of items to add
+          unpacked_objects: Iterator of UnpackedObject instances
+          progress: Optional progress reporting function.
         """
         """
         if count == 0:
         if count == 0:
             return
             return
@@ -1839,6 +1853,7 @@ class MemoryObjectStore(BaseObjectStore):
             requested bytes are read.
             requested bytes are read.
           read_some: Read function that returns at least one byte, but may
           read_some: Read function that returns at least one byte, but may
             not return the number of bytes requested.
             not return the number of bytes requested.
+          progress: Optional progress reporting function.
         """
         """
         f, commit, abort = self.add_pack()
         f, commit, abort = self.add_pack()
         try:
         try:
@@ -2136,6 +2151,8 @@ class ObjectStoreGraphWalker:
         Args:
         Args:
           local_heads: Heads to start search with
           local_heads: Heads to start search with
           get_parents: Function for finding the parents of a SHA1.
           get_parents: Function for finding the parents of a SHA1.
+          shallow: Set of shallow commits.
+          update_shallow: Function to update shallow commits.
         """
         """
         self.heads = set(local_heads)
         self.heads = set(local_heads)
         self.get_parents = get_parents
         self.get_parents = get_parents
@@ -2533,9 +2550,11 @@ def _collect_ancestors(
     """Collect all ancestors of heads up to (excluding) those in common.
     """Collect all ancestors of heads up to (excluding) those in common.
 
 
     Args:
     Args:
+      store: Object store to get commits from
       heads: commits to start from
       heads: commits to start from
       common: commits to end at, or empty set to walk repository
       common: commits to end at, or empty set to walk repository
         completely
         completely
+      shallow: Set of shallow commits
       get_parents: Optional function for getting the parents of a
       get_parents: Optional function for getting the parents of a
         commit.
         commit.
     Returns: a tuple (A, B) where A - all commits reachable
     Returns: a tuple (A, B) where A - all commits reachable
@@ -2581,6 +2600,7 @@ def iter_tree_contents(
     Iteration is depth-first pre-order, as in e.g. os.walk.
     Iteration is depth-first pre-order, as in e.g. os.walk.
 
 
     Args:
     Args:
+      store: Object store to get trees from
       tree_id: SHA1 of the tree.
       tree_id: SHA1 of the tree.
       include_trees: If True, include tree objects in the iteration.
       include_trees: If True, include tree objects in the iteration.
     Returns: Iterator over TreeEntry namedtuples for all the objects in a
     Returns: Iterator over TreeEntry namedtuples for all the objects in a
@@ -2608,6 +2628,7 @@ def peel_sha(store: ObjectContainer, sha: bytes) -> tuple[ShaFile, ShaFile]:
     """Peel all tags from a SHA.
     """Peel all tags from a SHA.
 
 
     Args:
     Args:
+      store: Object store to get objects from
       sha: The object SHA to peel.
       sha: The object SHA to peel.
     Returns: The fully-peeled SHA1 of a tag object, after peeling all
     Returns: The fully-peeled SHA1 of a tag object, after peeling all
         intermediate tags; if the original ref does not point to a tag,
         intermediate tags; if the original ref does not point to a tag,

+ 106 - 0
dulwich/objects.py

@@ -135,6 +135,14 @@ def hex_to_sha(hex: Union[bytes, str]) -> bytes:
 
 
 
 
 def valid_hexsha(hex: Union[bytes, str]) -> bool:
 def valid_hexsha(hex: Union[bytes, str]) -> bool:
+    """Check if a string is a valid hex SHA.
+
+    Args:
+      hex: Hex string to check
+
+    Returns:
+      True if valid hex SHA, False otherwise
+    """
     if len(hex) != 40:
     if len(hex) != 40:
         return False
         return False
     try:
     try:
@@ -185,10 +193,24 @@ def serializable_property(name: str, docstring: Optional[str] = None) -> propert
     """A property that helps tracking whether serialization is necessary."""
     """A property that helps tracking whether serialization is necessary."""
 
 
     def set(obj: "ShaFile", value: object) -> None:
     def set(obj: "ShaFile", value: object) -> None:
+        """Set the property value and mark the object as needing serialization.
+
+        Args:
+          obj: The ShaFile object
+          value: The value to set
+        """
         setattr(obj, "_" + name, value)
         setattr(obj, "_" + name, value)
         obj._needs_serialization = True
         obj._needs_serialization = True
 
 
     def get(obj: "ShaFile") -> object:
     def get(obj: "ShaFile") -> object:
+        """Get the property value.
+
+        Args:
+          obj: The ShaFile object
+
+        Returns:
+          The property value
+        """
         return getattr(obj, "_" + name)
         return getattr(obj, "_" + name)
 
 
     return property(get, set, doc=docstring)
     return property(get, set, doc=docstring)
@@ -276,6 +298,11 @@ class FixedSha:
     __slots__ = ("_hexsha", "_sha")
     __slots__ = ("_hexsha", "_sha")
 
 
     def __init__(self, hexsha: Union[str, bytes]) -> None:
     def __init__(self, hexsha: Union[str, bytes]) -> None:
+        """Initialize FixedSha with a fixed SHA value.
+
+        Args:
+            hexsha: Hex SHA value as string or bytes
+        """
         if isinstance(hexsha, str):
         if isinstance(hexsha, str):
             hexsha = hexsha.encode("ascii")
             hexsha = hexsha.encode("ascii")
         if not isinstance(hexsha, bytes):
         if not isinstance(hexsha, bytes):
@@ -623,6 +650,7 @@ class ShaFile:
         return self.sha().hexdigest().encode("ascii")
         return self.sha().hexdigest().encode("ascii")
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of this object."""
         return f"<{self.__class__.__name__} {self.id!r}>"
         return f"<{self.__class__.__name__} {self.id!r}>"
 
 
     def __ne__(self, other: object) -> bool:
     def __ne__(self, other: object) -> bool:
@@ -657,6 +685,7 @@ class Blob(ShaFile):
     _chunked_text: list[bytes]
     _chunked_text: list[bytes]
 
 
     def __init__(self) -> None:
     def __init__(self) -> None:
+        """Initialize a new Blob object."""
         super().__init__()
         super().__init__()
         self._chunked_text = []
         self._chunked_text = []
         self._needs_serialization = False
         self._needs_serialization = False
@@ -691,6 +720,17 @@ class Blob(ShaFile):
 
 
     @classmethod
     @classmethod
     def from_path(cls, path: Union[str, bytes]) -> "Blob":
     def from_path(cls, path: Union[str, bytes]) -> "Blob":
+        """Read a blob from a file on disk.
+
+        Args:
+          path: Path to the blob file
+
+        Returns:
+          A Blob object
+
+        Raises:
+          NotBlobError: If the file is not a blob
+        """
         blob = ShaFile.from_path(path)
         blob = ShaFile.from_path(path)
         if not isinstance(blob, cls):
         if not isinstance(blob, cls):
             raise NotBlobError(_path_to_bytes(path))
             raise NotBlobError(_path_to_bytes(path))
@@ -830,6 +870,7 @@ class Tag(ShaFile):
     _tagger: Optional[bytes]
     _tagger: Optional[bytes]
 
 
     def __init__(self) -> None:
     def __init__(self) -> None:
+        """Initialize a new Tag object."""
         super().__init__()
         super().__init__()
         self._tagger = None
         self._tagger = None
         self._tag_time = None
         self._tag_time = None
@@ -839,6 +880,17 @@ class Tag(ShaFile):
 
 
     @classmethod
     @classmethod
     def from_path(cls, filename: Union[str, bytes]) -> "Tag":
     def from_path(cls, filename: Union[str, bytes]) -> "Tag":
+        """Read a tag from a file on disk.
+
+        Args:
+          filename: Path to the tag file
+
+        Returns:
+          A Tag object
+
+        Raises:
+          NotTagError: If the file is not a tag
+        """
         tag = ShaFile.from_path(filename)
         tag = ShaFile.from_path(filename)
         if not isinstance(tag, cls):
         if not isinstance(tag, cls):
             raise NotTagError(_path_to_bytes(filename))
             raise NotTagError(_path_to_bytes(filename))
@@ -991,6 +1043,12 @@ class Tag(ShaFile):
     signature = serializable_property("signature", "Optional detached GPG signature")
     signature = serializable_property("signature", "Optional detached GPG signature")
 
 
     def sign(self, keyid: Optional[str] = None) -> None:
     def sign(self, keyid: Optional[str] = None) -> None:
+        """Sign this tag with a GPG key.
+
+        Args:
+          keyid: Optional GPG key ID to use for signing. If not specified,
+                 the default GPG key will be used.
+        """
         import gpg
         import gpg
 
 
         with gpg.Context(armor=True) as c:
         with gpg.Context(armor=True) as c:
@@ -1065,6 +1123,7 @@ def parse_tree(text: bytes, strict: bool = False) -> Iterator[tuple[bytes, int,
 
 
     Args:
     Args:
       text: Serialized text to parse
       text: Serialized text to parse
+      strict: If True, enforce strict validation
     Returns: iterator of tuples of (name, mode, sha)
     Returns: iterator of tuples of (name, mode, sha)
 
 
     Raises:
     Raises:
@@ -1155,6 +1214,7 @@ def pretty_format_tree_entry(
       name: Name of the directory entry
       name: Name of the directory entry
       mode: Mode of entry
       mode: Mode of entry
       hexsha: Hexsha of the referenced object
       hexsha: Hexsha of the referenced object
+      encoding: Character encoding for the name
     Returns: string describing the tree entry
     Returns: string describing the tree entry
     """
     """
     if mode & stat.S_IFDIR:
     if mode & stat.S_IFDIR:
@@ -1173,6 +1233,12 @@ class SubmoduleEncountered(Exception):
     """A submodule was encountered while resolving a path."""
     """A submodule was encountered while resolving a path."""
 
 
     def __init__(self, path: bytes, sha: ObjectID) -> None:
     def __init__(self, path: bytes, sha: ObjectID) -> None:
+        """Initialize SubmoduleEncountered exception.
+
+        Args:
+            path: Path where the submodule was encountered
+            sha: SHA of the submodule
+        """
         self.path = path
         self.path = path
         self.sha = sha
         self.sha = sha
 
 
@@ -1186,20 +1252,34 @@ class Tree(ShaFile):
     __slots__ = "_entries"
     __slots__ = "_entries"
 
 
     def __init__(self) -> None:
     def __init__(self) -> None:
+        """Initialize an empty Tree."""
         super().__init__()
         super().__init__()
         self._entries: dict[bytes, tuple[int, bytes]] = {}
         self._entries: dict[bytes, tuple[int, bytes]] = {}
 
 
     @classmethod
     @classmethod
     def from_path(cls, filename: Union[str, bytes]) -> "Tree":
     def from_path(cls, filename: Union[str, bytes]) -> "Tree":
+        """Read a tree from a file on disk.
+
+        Args:
+          filename: Path to the tree file
+
+        Returns:
+          A Tree object
+
+        Raises:
+          NotTreeError: If the file is not a tree
+        """
         tree = ShaFile.from_path(filename)
         tree = ShaFile.from_path(filename)
         if not isinstance(tree, cls):
         if not isinstance(tree, cls):
             raise NotTreeError(_path_to_bytes(filename))
             raise NotTreeError(_path_to_bytes(filename))
         return tree
         return tree
 
 
     def __contains__(self, name: bytes) -> bool:
     def __contains__(self, name: bytes) -> bool:
+        """Check if name exists in tree."""
         return name in self._entries
         return name in self._entries
 
 
     def __getitem__(self, name: bytes) -> tuple[int, ObjectID]:
     def __getitem__(self, name: bytes) -> tuple[int, ObjectID]:
+        """Get tree entry by name."""
         return self._entries[name]
         return self._entries[name]
 
 
     def __setitem__(self, name: bytes, value: tuple[int, ObjectID]) -> None:
     def __setitem__(self, name: bytes, value: tuple[int, ObjectID]) -> None:
@@ -1216,13 +1296,16 @@ class Tree(ShaFile):
         self._needs_serialization = True
         self._needs_serialization = True
 
 
     def __delitem__(self, name: bytes) -> None:
     def __delitem__(self, name: bytes) -> None:
+        """Delete tree entry by name."""
         del self._entries[name]
         del self._entries[name]
         self._needs_serialization = True
         self._needs_serialization = True
 
 
     def __len__(self) -> int:
     def __len__(self) -> int:
+        """Return number of entries in tree."""
         return len(self._entries)
         return len(self._entries)
 
 
     def __iter__(self) -> Iterator[bytes]:
     def __iter__(self) -> Iterator[bytes]:
+        """Iterate over tree entry names."""
         return iter(self._entries)
         return iter(self._entries)
 
 
     def add(self, name: bytes, mode: int, hexsha: bytes) -> None:
     def add(self, name: bytes, mode: int, hexsha: bytes) -> None:
@@ -1305,6 +1388,11 @@ class Tree(ShaFile):
         return list(serialize_tree(self.iteritems()))
         return list(serialize_tree(self.iteritems()))
 
 
     def as_pretty_string(self) -> str:
     def as_pretty_string(self) -> str:
+        """Return a human-readable string representation of this tree.
+
+        Returns:
+          Pretty-printed tree entries
+        """
         text: list[str] = []
         text: list[str] = []
         for name, mode, hexsha in self.iteritems():
         for name, mode, hexsha in self.iteritems():
             text.append(pretty_format_tree_entry(name, mode, hexsha))
             text.append(pretty_format_tree_entry(name, mode, hexsha))
@@ -1530,6 +1618,7 @@ class Commit(ShaFile):
     )
     )
 
 
     def __init__(self) -> None:
     def __init__(self) -> None:
+        """Initialize an empty Commit."""
         super().__init__()
         super().__init__()
         self._parents: list[bytes] = []
         self._parents: list[bytes] = []
         self._encoding: Optional[bytes] = None
         self._encoding: Optional[bytes] = None
@@ -1541,6 +1630,17 @@ class Commit(ShaFile):
 
 
     @classmethod
     @classmethod
     def from_path(cls, path: Union[str, bytes]) -> "Commit":
     def from_path(cls, path: Union[str, bytes]) -> "Commit":
+        """Read a commit from a file on disk.
+
+        Args:
+          path: Path to the commit file
+
+        Returns:
+          A Commit object
+
+        Raises:
+          NotCommitError: If the file is not a commit
+        """
         commit = ShaFile.from_path(path)
         commit = ShaFile.from_path(path)
         if not isinstance(commit, cls):
         if not isinstance(commit, cls):
             raise NotCommitError(_path_to_bytes(path))
             raise NotCommitError(_path_to_bytes(path))
@@ -1653,6 +1753,12 @@ class Commit(ShaFile):
         # TODO: optionally check for duplicate parents
         # TODO: optionally check for duplicate parents
 
 
     def sign(self, keyid: Optional[str] = None) -> None:
     def sign(self, keyid: Optional[str] = None) -> None:
+        """Sign this commit with a GPG key.
+
+        Args:
+          keyid: Optional GPG key ID to use for signing. If not specified,
+                 the default GPG key will be used.
+        """
         import gpg
         import gpg
 
 
         with gpg.Context(armor=True) as c:
         with gpg.Context(armor=True) as c:

+ 15 - 0
dulwich/objectspec.py

@@ -32,6 +32,14 @@ if TYPE_CHECKING:
 
 
 
 
 def to_bytes(text: Union[str, bytes]) -> bytes:
 def to_bytes(text: Union[str, bytes]) -> bytes:
+    """Convert text to bytes.
+
+    Args:
+      text: Text to convert (str or bytes)
+
+    Returns:
+      Bytes representation of text
+    """
     if getattr(text, "encode", None) is not None:
     if getattr(text, "encode", None) is not None:
         text = text.encode("ascii")  # type: ignore
         text = text.encode("ascii")  # type: ignore
     return text  # type: ignore
     return text  # type: ignore
@@ -233,6 +241,7 @@ def parse_reftuple(
       lh_container: A RefsContainer object
       lh_container: A RefsContainer object
       rh_container: A RefsContainer object
       rh_container: A RefsContainer object
       refspec: A string
       refspec: A string
+      force: Whether to force the operation
     Returns: A tuple with left and right ref
     Returns: A tuple with left and right ref
     Raises:
     Raises:
       KeyError: If one of the refs can not be found
       KeyError: If one of the refs can not be found
@@ -348,6 +357,12 @@ class AmbiguousShortId(Exception):
     """The short id is ambiguous."""
     """The short id is ambiguous."""
 
 
     def __init__(self, prefix: bytes, options: list[ShaFile]) -> None:
     def __init__(self, prefix: bytes, options: list[ShaFile]) -> None:
+        """Initialize AmbiguousShortId.
+
+        Args:
+          prefix: The ambiguous prefix
+          options: List of matching objects
+        """
         self.prefix = prefix
         self.prefix = prefix
         self.options = options
         self.options = options
 
 

Разница между файлами не показана из-за своего большого размера
+ 437 - 4
dulwich/pack.py


+ 48 - 2
dulwich/patch.py

@@ -58,8 +58,12 @@ def write_commit_patch(
     """Write a individual file patch.
     """Write a individual file patch.
 
 
     Args:
     Args:
+      f: File-like object to write to
       commit: Commit object
       commit: Commit object
+      contents: Contents of the patch
       progress: tuple with current patch number and total.
       progress: tuple with current patch number and total.
+      version: Version string to include in patch header
+      encoding: Encoding to use for the patch
 
 
     Returns:
     Returns:
       tuple with filename and contents
       tuple with filename and contents
@@ -147,8 +151,7 @@ def unified_diff(
     tree_encoding: str = "utf-8",
     tree_encoding: str = "utf-8",
     output_encoding: str = "utf-8",
     output_encoding: str = "utf-8",
 ) -> Generator[bytes, None, None]:
 ) -> Generator[bytes, None, None]:
-    """difflib.unified_diff that can detect "No newline at end of file" as
-    original "git diff" does.
+    """difflib.unified_diff that can detect "No newline at end of file" as original "git diff" does.
 
 
     Based on the same function in Python2.7 difflib.py
     Based on the same function in Python2.7 difflib.py
     """
     """
@@ -197,6 +200,14 @@ def is_binary(content: bytes) -> bool:
 
 
 
 
 def shortid(hexsha: Optional[bytes]) -> bytes:
 def shortid(hexsha: Optional[bytes]) -> bytes:
+    """Get short object ID.
+
+    Args:
+        hexsha: Full hex SHA or None
+
+    Returns:
+        7-character short ID
+    """
     if hexsha is None:
     if hexsha is None:
         return b"0" * 7
         return b"0" * 7
     else:
     else:
@@ -204,6 +215,15 @@ def shortid(hexsha: Optional[bytes]) -> bytes:
 
 
 
 
 def patch_filename(p: Optional[bytes], root: bytes) -> bytes:
 def patch_filename(p: Optional[bytes], root: bytes) -> bytes:
+    """Generate patch filename.
+
+    Args:
+        p: Path or None
+        root: Root directory
+
+    Returns:
+        Full patch filename
+    """
     if p is None:
     if p is None:
         return b"/dev/null"
         return b"/dev/null"
     else:
     else:
@@ -235,6 +255,15 @@ def write_object_diff(
     patched_new_path = patch_filename(new_path, b"b")
     patched_new_path = patch_filename(new_path, b"b")
 
 
     def content(mode: Optional[int], hexsha: Optional[bytes]) -> Blob:
     def content(mode: Optional[int], hexsha: Optional[bytes]) -> Blob:
+        """Get blob content for a file.
+
+        Args:
+            mode: File mode
+            hexsha: Object SHA
+
+        Returns:
+            Blob object
+        """
         from typing import cast
         from typing import cast
 
 
         if hexsha is None:
         if hexsha is None:
@@ -250,6 +279,14 @@ def write_object_diff(
                 return cast(Blob, Blob.from_string(obj.as_raw_string()))
                 return cast(Blob, Blob.from_string(obj.as_raw_string()))
 
 
     def lines(content: "Blob") -> list[bytes]:
     def lines(content: "Blob") -> list[bytes]:
+        """Split blob content into lines.
+
+        Args:
+            content: Blob content
+
+        Returns:
+            List of lines
+        """
         if not content:
         if not content:
             return []
             return []
         else:
         else:
@@ -338,6 +375,14 @@ def write_blob_diff(
     patched_new_path = patch_filename(new_path, b"b")
     patched_new_path = patch_filename(new_path, b"b")
 
 
     def lines(blob: Optional["Blob"]) -> list[bytes]:
     def lines(blob: Optional["Blob"]) -> list[bytes]:
+        """Split blob content into lines.
+
+        Args:
+            blob: Blob object or None
+
+        Returns:
+            List of lines
+        """
         if blob is not None:
         if blob is not None:
             return blob.splitlines()
             return blob.splitlines()
         else:
         else:
@@ -368,6 +413,7 @@ def write_tree_diff(
 
 
     Args:
     Args:
       f: File-like object to write to.
       f: File-like object to write to.
+      store: Object store to read from
       old_tree: Old tree id
       old_tree: Old tree id
       new_tree: New tree id
       new_tree: New tree id
       diff_binary: Whether to diff files even if they
       diff_binary: Whether to diff files even if they

+ 34 - 11
dulwich/porcelain.py

@@ -334,8 +334,8 @@ def parse_timezone_format(tz_str):
 
 
 
 
 def get_user_timezones():
 def get_user_timezones():
-    """Retrieve local timezone as described in
-    https://raw.githubusercontent.com/git/git/v2.3.0/Documentation/date-formats.txt
+    """Retrieve local timezone as described in https://raw.githubusercontent.com/git/git/v2.3.0/Documentation/date-formats.txt.
+
     Returns: A tuple containing author timezone, committer timezone.
     Returns: A tuple containing author timezone, committer timezone.
     """
     """
     local_timezone = time.localtime().tm_gmtoff
     local_timezone = time.localtime().tm_gmtoff
@@ -391,6 +391,7 @@ def open_repo_closing(
     path_or_repo: Union[str, os.PathLike, T],
     path_or_repo: Union[str, os.PathLike, T],
 ) -> AbstractContextManager[Union[T, Repo]]:
 ) -> AbstractContextManager[Union[T, Repo]]:
     """Open an argument that can be a repository or a path for a repository.
     """Open an argument that can be a repository or a path for a repository.
+
     returns a context manager that will close the repo on exit if the argument
     returns a context manager that will close the repo on exit if the argument
     is a path, else does nothing if the argument is a repo.
     is a path, else does nothing if the argument is a repo.
     """
     """
@@ -402,12 +403,12 @@ def open_repo_closing(
 def path_to_tree_path(
 def path_to_tree_path(
     repopath: Union[str, os.PathLike], path, tree_encoding=DEFAULT_ENCODING
     repopath: Union[str, os.PathLike], path, tree_encoding=DEFAULT_ENCODING
 ):
 ):
-    """Convert a path to a path usable in an index, e.g. bytes and relative to
-    the repository root.
+    """Convert a path to a path usable in an index, e.g. bytes and relative to the repository root.
 
 
     Args:
     Args:
       repopath: Repository path, absolute or relative to the cwd
       repopath: Repository path, absolute or relative to the cwd
       path: A path, absolute or relative to the cwd
       path: A path, absolute or relative to the cwd
+      tree_encoding: Encoding to use for tree paths
     Returns: A path formatted for use in e.g. an index
     Returns: A path formatted for use in e.g. an index
     """
     """
     # Resolve might returns a relative path on Windows
     # Resolve might returns a relative path on Windows
@@ -570,6 +571,7 @@ def commit(
       author_timezone: Author timestamp timezone
       author_timezone: Author timestamp timezone
       committer: Optional committer name and email
       committer: Optional committer name and email
       commit_timezone: Commit timestamp timezone
       commit_timezone: Commit timestamp timezone
+      encoding: Encoding to use for commit message
       no_verify: Skip pre-commit and commit-msg hooks
       no_verify: Skip pre-commit and commit-msg hooks
       signoff: GPG Sign the commit (bool, defaults to False,
       signoff: GPG Sign the commit (bool, defaults to False,
         pass True to use default GPG key,
         pass True to use default GPG key,
@@ -664,6 +666,7 @@ def commit_tree(
     Args:
     Args:
       repo: Path to repository
       repo: Path to repository
       tree: An existing tree object
       tree: An existing tree object
+      message: Commit message
       author: Optional author name and email
       author: Optional author name and email
       committer: Optional committer name and email
       committer: Optional committer name and email
     """
     """
@@ -729,10 +732,8 @@ def clone(
       protocol_version: desired Git protocol version. By default the highest
       protocol_version: desired Git protocol version. By default the highest
         mutually supported protocol version will be used.
         mutually supported protocol version will be used.
       recurse_submodules: Whether to initialize and clone submodules
       recurse_submodules: Whether to initialize and clone submodules
-
-    Keyword Args:
-      refspecs: refspecs to fetch. Can be a bytestring, a string, or a list of
-        bytestring/string.
+      **kwargs: Additional keyword arguments including refspecs to fetch.
+        Can be a bytestring, a string, or a list of bytestring/string.
 
 
     Returns: The new repository
     Returns: The new repository
     """
     """
@@ -977,6 +978,7 @@ def remove(repo: Union[str, os.PathLike, Repo] = ".", paths=None, cached=False)
     Args:
     Args:
       repo: Repository for the files
       repo: Repository for the files
       paths: Paths to remove. Can be absolute or relative to the repository root.
       paths: Paths to remove. Can be absolute or relative to the repository root.
+      cached: Only remove from index, not from working directory
     """
     """
     with open_repo_closing(repo) as r:
     with open_repo_closing(repo) as r:
         index = r.open_index()
         index = r.open_index()
@@ -1180,6 +1182,7 @@ def print_commit(commit, decode, outstream=sys.stdout) -> None:
 
 
     Args:
     Args:
       commit: A `Commit` object
       commit: A `Commit` object
+      decode: Function to decode commit data
       outstream: A stream file to write to
       outstream: A stream file to write to
     """
     """
     outstream.write("-" * 50 + "\n")
     outstream.write("-" * 50 + "\n")
@@ -1247,10 +1250,22 @@ def show_commit(repo: RepoPath, commit, decode, outstream=sys.stdout) -> None:
 
 
     # Create a wrapper for ColorizedDiffStream to handle string/bytes conversion
     # Create a wrapper for ColorizedDiffStream to handle string/bytes conversion
     class _StreamWrapper:
     class _StreamWrapper:
+        """Wrapper for ColorizedDiffStream to handle string/bytes conversion."""
+
         def __init__(self, stream):
         def __init__(self, stream):
+            """Initialize a _StreamWrapper.
+
+            Args:
+              stream: The underlying stream to wrap
+            """
             self.stream = stream
             self.stream = stream
 
 
         def write(self, data):
         def write(self, data):
+            """Write data to the stream, converting strings to bytes if needed.
+
+            Args:
+              data: Data to write (str or bytes)
+            """
             if isinstance(data, str):
             if isinstance(data, str):
                 # Convert string to bytes for ColorizedDiffStream
                 # Convert string to bytes for ColorizedDiffStream
                 self.stream.write(data.encode("utf-8"))
                 self.stream.write(data.encode("utf-8"))
@@ -1639,6 +1654,7 @@ def submodule_update(
       paths: Optional list of specific submodule paths to update. If None, updates all.
       paths: Optional list of specific submodule paths to update. If None, updates all.
       init: If True, initialize submodules first
       init: If True, initialize submodules first
       force: Force update even if local changes exist
       force: Force update even if local changes exist
+      errstream: Error stream for error messages
     """
     """
     from .submodule import iter_cached_submodules
     from .submodule import iter_cached_submodules
 
 
@@ -1781,6 +1797,7 @@ def tag_create(
       sign: GPG Sign the tag (bool, defaults to False,
       sign: GPG Sign the tag (bool, defaults to False,
         pass True to use default GPG key,
         pass True to use default GPG key,
         pass a str containing Key ID to use a specific GPG key)
         pass a str containing Key ID to use a specific GPG key)
+      encoding: Encoding to use for tag messages
     """
     """
     with open_repo_closing(repo) as r:
     with open_repo_closing(repo) as r:
         object = parse_object(r, objectish)
         object = parse_object(r, objectish)
@@ -2173,6 +2190,7 @@ def push(
       outstream: A stream file to write output
       outstream: A stream file to write output
       errstream: A stream file to write errors
       errstream: A stream file to write errors
       force: Force overwriting refs
       force: Force overwriting refs
+      **kwargs: Additional keyword arguments for the client
     """
     """
     # Open the repo
     # Open the repo
     with open_repo_closing(repo) as r:
     with open_repo_closing(repo) as r:
@@ -2301,6 +2319,7 @@ def pull(
         feature, and ignored otherwise.
         feature, and ignored otherwise.
       protocol_version: desired Git protocol version. By default the highest
       protocol_version: desired Git protocol version. By default the highest
         mutually supported protocol version will be used
         mutually supported protocol version will be used
+      **kwargs: Additional keyword arguments for the client
     """
     """
     # Open the repo
     # Open the repo
     with open_repo_closing(repo) as r:
     with open_repo_closing(repo) as r:
@@ -3019,6 +3038,8 @@ def fetch(
       depth: Depth to fetch at
       depth: Depth to fetch at
       prune: Prune remote removed refs
       prune: Prune remote removed refs
       prune_tags: Prune reomte removed tags
       prune_tags: Prune reomte removed tags
+      force: Force fetching even if it would overwrite local changes
+      **kwargs: Additional keyword arguments for the client
     Returns:
     Returns:
       Dictionary with refs on the remote
       Dictionary with refs on the remote
     """
     """
@@ -3108,6 +3129,7 @@ def ls_remote(remote, config: Optional[Config] = None, **kwargs):
     Args:
     Args:
       remote: Remote repository location
       remote: Remote repository location
       config: Configuration to use
       config: Configuration to use
+      **kwargs: Additional keyword arguments for the client
     Returns:
     Returns:
       LsRemoteResult object with refs and symrefs
       LsRemoteResult object with refs and symrefs
     """
     """
@@ -3616,6 +3638,7 @@ def reset_file(
       repo: dulwich Repo object
       repo: dulwich Repo object
       file_path: file to reset, relative to the repository path
       file_path: file to reset, relative to the repository path
       target: branch or commit or b'HEAD' to reset
       target: branch or commit or b'HEAD' to reset
+      symlink_fn: Function to use for creating symlinks
     """
     """
     tree = parse_tree(repo, treeish=target)
     tree = parse_tree(repo, treeish=target)
     tree_path = _fs_to_tree_path(file_path)
     tree_path = _fs_to_tree_path(file_path)
@@ -4249,7 +4272,7 @@ def merge_tree(
         return merged_tree.id, conflicts
         return merged_tree.id, conflicts
 
 
 
 
-def cherry_pick(
+def cherry_pick(  # noqa: D417
     repo: Union[str, os.PathLike, Repo],
     repo: Union[str, os.PathLike, Repo],
     committish: Union[str, bytes, Commit, Tag, None],
     committish: Union[str, bytes, Commit, Tag, None],
     no_commit=False,
     no_commit=False,
@@ -4260,9 +4283,9 @@ def cherry_pick(
 
 
     Args:
     Args:
       repo: Repository to cherry-pick into
       repo: Repository to cherry-pick into
-      committish: Commit to cherry-pick (can be None only when ``continue_`` or abort is True)
+      committish: Commit to cherry-pick (can be None only when resuming or aborting)
       no_commit: If True, do not create a commit after applying changes
       no_commit: If True, do not create a commit after applying changes
-      ``continue_``: Continue an in-progress cherry-pick after resolving conflicts
+      ``continue_``: Resume an in-progress cherry-pick after resolving conflicts if True
       abort: Abort an in-progress cherry-pick
       abort: Abort an in-progress cherry-pick
 
 
     Returns:
     Returns:

+ 144 - 0
dulwich/protocol.py

@@ -131,22 +131,57 @@ NAK_LINE = b"NAK\n"
 
 
 
 
 def agent_string() -> bytes:
 def agent_string() -> bytes:
+    """Generate the agent string for dulwich.
+
+    Returns:
+      Agent string as bytes
+    """
     return ("dulwich/" + ".".join(map(str, dulwich.__version__))).encode("ascii")
     return ("dulwich/" + ".".join(map(str, dulwich.__version__))).encode("ascii")
 
 
 
 
 def capability_agent() -> bytes:
 def capability_agent() -> bytes:
+    """Generate the agent capability string.
+
+    Returns:
+      Agent capability with dulwich version
+    """
     return CAPABILITY_AGENT + b"=" + agent_string()
     return CAPABILITY_AGENT + b"=" + agent_string()
 
 
 
 
 def capability_symref(from_ref: bytes, to_ref: bytes) -> bytes:
 def capability_symref(from_ref: bytes, to_ref: bytes) -> bytes:
+    """Generate a symref capability string.
+
+    Args:
+      from_ref: Source reference name
+      to_ref: Target reference name
+
+    Returns:
+      Symref capability string
+    """
     return CAPABILITY_SYMREF + b"=" + from_ref + b":" + to_ref
     return CAPABILITY_SYMREF + b"=" + from_ref + b":" + to_ref
 
 
 
 
 def extract_capability_names(capabilities: Iterable[bytes]) -> set[bytes]:
 def extract_capability_names(capabilities: Iterable[bytes]) -> set[bytes]:
+    """Extract capability names from a list of capabilities.
+
+    Args:
+      capabilities: List of capability strings
+
+    Returns:
+      Set of capability names
+    """
     return {parse_capability(c)[0] for c in capabilities}
     return {parse_capability(c)[0] for c in capabilities}
 
 
 
 
 def parse_capability(capability: bytes) -> tuple[bytes, Optional[bytes]]:
 def parse_capability(capability: bytes) -> tuple[bytes, Optional[bytes]]:
+    """Parse a capability string into name and value.
+
+    Args:
+      capability: Capability string
+
+    Returns:
+      Tuple of (capability_name, capability_value)
+    """
     parts = capability.split(b"=", 1)
     parts = capability.split(b"=", 1)
     if len(parts) == 1:
     if len(parts) == 1:
         return (parts[0], None)
         return (parts[0], None)
@@ -154,6 +189,14 @@ def parse_capability(capability: bytes) -> tuple[bytes, Optional[bytes]]:
 
 
 
 
 def symref_capabilities(symrefs: Iterable[tuple[bytes, bytes]]) -> list[bytes]:
 def symref_capabilities(symrefs: Iterable[tuple[bytes, bytes]]) -> list[bytes]:
+    """Generate symref capability strings from symref pairs.
+
+    Args:
+      symrefs: Iterable of (from_ref, to_ref) tuples
+
+    Returns:
+      List of symref capability strings
+    """
     return [capability_symref(*k) for k in symrefs]
     return [capability_symref(*k) for k in symrefs]
 
 
 
 
@@ -166,10 +209,27 @@ COMMAND_HAVE = b"have"
 
 
 
 
 def format_cmd_pkt(cmd: bytes, *args: bytes) -> bytes:
 def format_cmd_pkt(cmd: bytes, *args: bytes) -> bytes:
+    """Format a command packet.
+
+    Args:
+      cmd: Command name
+      *args: Command arguments
+
+    Returns:
+      Formatted command packet
+    """
     return cmd + b" " + b"".join([(a + b"\0") for a in args])
     return cmd + b" " + b"".join([(a + b"\0") for a in args])
 
 
 
 
 def parse_cmd_pkt(line: bytes) -> tuple[bytes, list[bytes]]:
 def parse_cmd_pkt(line: bytes) -> tuple[bytes, list[bytes]]:
+    """Parse a command packet.
+
+    Args:
+      line: Command line to parse
+
+    Returns:
+      Tuple of (command, [arguments])
+    """
     splice_at = line.find(b" ")
     splice_at = line.find(b" ")
     cmd, args = line[:splice_at], line[splice_at + 1 :]
     cmd, args = line[:splice_at], line[splice_at + 1 :]
     assert args[-1:] == b"\x00"
     assert args[-1:] == b"\x00"
@@ -229,6 +289,14 @@ class Protocol:
         close: Optional[Callable[[], None]] = None,
         close: Optional[Callable[[], None]] = None,
         report_activity: Optional[Callable[[int, str], None]] = None,
         report_activity: Optional[Callable[[int, str], None]] = None,
     ) -> None:
     ) -> None:
+        """Initialize Protocol.
+
+        Args:
+          read: Function to read bytes from the transport
+          write: Function to write bytes to the transport
+          close: Optional function to close the transport
+          report_activity: Optional function to report activity
+        """
         self.read = read
         self.read = read
         self.write = write
         self.write = write
         self._close = close
         self._close = close
@@ -236,10 +304,12 @@ class Protocol:
         self._readahead: Optional[BytesIO] = None
         self._readahead: Optional[BytesIO] = None
 
 
     def close(self) -> None:
     def close(self) -> None:
+        """Close the underlying transport if a close function was provided."""
         if self._close:
         if self._close:
             self._close()
             self._close()
 
 
     def __enter__(self) -> "Protocol":
     def __enter__(self) -> "Protocol":
+        """Enter context manager."""
         return self
         return self
 
 
     def __exit__(
     def __exit__(
@@ -248,6 +318,7 @@ class Protocol:
         exc_val: Optional[BaseException],
         exc_val: Optional[BaseException],
         exc_tb: Optional[types.TracebackType],
         exc_tb: Optional[types.TracebackType],
     ) -> None:
     ) -> None:
+        """Exit context manager and close transport."""
         self.close()
         self.close()
 
 
     def read_pkt_line(self) -> Optional[bytes]:
     def read_pkt_line(self) -> Optional[bytes]:
@@ -405,12 +476,29 @@ class ReceivableProtocol(Protocol):
         report_activity: Optional[Callable[[int, str], None]] = None,
         report_activity: Optional[Callable[[int, str], None]] = None,
         rbufsize: int = _RBUFSIZE,
         rbufsize: int = _RBUFSIZE,
     ) -> None:
     ) -> None:
+        """Initialize ReceivableProtocol.
+
+        Args:
+          recv: Function to receive bytes from the transport
+          write: Function to write bytes to the transport
+          close: Optional function to close the transport
+          report_activity: Optional function to report activity
+          rbufsize: Read buffer size
+        """
         super().__init__(self.read, write, close=close, report_activity=report_activity)
         super().__init__(self.read, write, close=close, report_activity=report_activity)
         self._recv = recv
         self._recv = recv
         self._rbuf = BytesIO()
         self._rbuf = BytesIO()
         self._rbufsize = rbufsize
         self._rbufsize = rbufsize
 
 
     def read(self, size: int) -> bytes:
     def read(self, size: int) -> bytes:
+        """Read bytes from the socket.
+
+        Args:
+          size: Number of bytes to read
+
+        Returns:
+          Bytes read from socket
+        """
         # From _fileobj.read in socket.py in the Python 2.6.5 standard library,
         # From _fileobj.read in socket.py in the Python 2.6.5 standard library,
         # with the following modifications:
         # with the following modifications:
         #  - omit the size <= 0 branch
         #  - omit the size <= 0 branch
@@ -472,6 +560,14 @@ class ReceivableProtocol(Protocol):
         return buf.read()
         return buf.read()
 
 
     def recv(self, size: int) -> bytes:
     def recv(self, size: int) -> bytes:
+        """Receive bytes from the socket with buffering.
+
+        Args:
+          size: Maximum number of bytes to receive
+
+        Returns:
+          Bytes received from socket
+        """
         assert size > 0
         assert size > 0
 
 
         buf = self._rbuf
         buf = self._rbuf
@@ -585,6 +681,11 @@ class PktLineParser:
     """Packet line parser that hands completed packets off to a callback."""
     """Packet line parser that hands completed packets off to a callback."""
 
 
     def __init__(self, handle_pkt: Callable[[Optional[bytes]], None]) -> None:
     def __init__(self, handle_pkt: Callable[[Optional[bytes]], None]) -> None:
+        """Initialize PktLineParser.
+
+        Args:
+          handle_pkt: Callback function to handle completed packets
+        """
         self.handle_pkt = handle_pkt
         self.handle_pkt = handle_pkt
         self._readahead = BytesIO()
         self._readahead = BytesIO()
 
 
@@ -613,12 +714,30 @@ class PktLineParser:
 
 
 
 
 def format_capability_line(capabilities: Iterable[bytes]) -> bytes:
 def format_capability_line(capabilities: Iterable[bytes]) -> bytes:
+    """Format a capabilities list for the wire protocol.
+
+    Args:
+      capabilities: List of capability strings
+
+    Returns:
+      Space-separated capabilities as bytes
+    """
     return b"".join([b" " + c for c in capabilities])
     return b"".join([b" " + c for c in capabilities])
 
 
 
 
 def format_ref_line(
 def format_ref_line(
     ref: bytes, sha: bytes, capabilities: Optional[list[bytes]] = None
     ref: bytes, sha: bytes, capabilities: Optional[list[bytes]] = None
 ) -> bytes:
 ) -> bytes:
+    """Format a ref advertisement line.
+
+    Args:
+      ref: Reference name
+      sha: SHA hash
+      capabilities: Optional list of capabilities
+
+    Returns:
+      Formatted ref line
+    """
     if capabilities is None:
     if capabilities is None:
         return sha + b" " + ref + b"\n"
         return sha + b" " + ref + b"\n"
     else:
     else:
@@ -626,14 +745,39 @@ def format_ref_line(
 
 
 
 
 def format_shallow_line(sha: bytes) -> bytes:
 def format_shallow_line(sha: bytes) -> bytes:
+    """Format a shallow line.
+
+    Args:
+      sha: SHA to mark as shallow
+
+    Returns:
+      Formatted shallow line
+    """
     return COMMAND_SHALLOW + b" " + sha
     return COMMAND_SHALLOW + b" " + sha
 
 
 
 
 def format_unshallow_line(sha: bytes) -> bytes:
 def format_unshallow_line(sha: bytes) -> bytes:
+    """Format an unshallow line.
+
+    Args:
+      sha: SHA to unshallow
+
+    Returns:
+      Formatted unshallow line
+    """
     return COMMAND_UNSHALLOW + b" " + sha
     return COMMAND_UNSHALLOW + b" " + sha
 
 
 
 
 def format_ack_line(sha: bytes, ack_type: bytes = b"") -> bytes:
 def format_ack_line(sha: bytes, ack_type: bytes = b"") -> bytes:
+    """Format an ACK line.
+
+    Args:
+      sha: SHA to acknowledge
+      ack_type: Optional ACK type (e.g. b"continue")
+
+    Returns:
+      Formatted ACK line
+    """
     if ack_type:
     if ack_type:
         ack_type = b" " + ack_type
         ack_type = b" " + ack_type
     return b"ACK " + sha + ack_type + b"\n"
     return b"ACK " + sha + ack_type + b"\n"

+ 10 - 0
dulwich/rebase.py

@@ -43,6 +43,11 @@ class RebaseConflict(RebaseError):
     """Raised when a rebase conflict occurs."""
     """Raised when a rebase conflict occurs."""
 
 
     def __init__(self, conflicted_files: list[bytes]):
     def __init__(self, conflicted_files: list[bytes]):
+        """Initialize RebaseConflict.
+
+        Args:
+          conflicted_files: List of conflicted file paths
+        """
         self.conflicted_files = conflicted_files
         self.conflicted_files = conflicted_files
         super().__init__(
         super().__init__(
             f"Conflicts in: {', '.join(f.decode('utf-8', 'replace') for f in conflicted_files)}"
             f"Conflicts in: {', '.join(f.decode('utf-8', 'replace') for f in conflicted_files)}"
@@ -525,6 +530,11 @@ class MemoryRebaseStateManager:
     """Manages rebase state in memory for MemoryRepo."""
     """Manages rebase state in memory for MemoryRepo."""
 
 
     def __init__(self, repo: Repo) -> None:
     def __init__(self, repo: Repo) -> None:
+        """Initialize MemoryRebaseStateManager.
+
+        Args:
+          repo: Repository instance
+        """
         self.repo = repo
         self.repo = repo
         self._state: Optional[dict] = None
         self._state: Optional[dict] = None
         self._todo: Optional[RebaseTodo] = None
         self._todo: Optional[RebaseTodo] = None

+ 236 - 0
dulwich/refs.py

@@ -56,6 +56,12 @@ class SymrefLoop(Exception):
     """There is a loop between one or more symrefs."""
     """There is a loop between one or more symrefs."""
 
 
     def __init__(self, ref, depth) -> None:
     def __init__(self, ref, depth) -> None:
+        """Initialize a SymrefLoop exception.
+
+        Args:
+          ref: The ref that caused the loop
+          depth: Depth at which the loop was detected
+        """
         self.ref = ref
         self.ref = ref
         self.depth = depth
         self.depth = depth
 
 
@@ -137,6 +143,11 @@ class RefsContainer:
     """A container for refs."""
     """A container for refs."""
 
 
     def __init__(self, logger=None) -> None:
     def __init__(self, logger=None) -> None:
+        """Initialize a RefsContainer.
+
+        Args:
+          logger: Optional logger for reflog updates
+        """
         self._logger = logger
         self._logger = logger
 
 
     def _log(
     def _log(
@@ -169,6 +180,9 @@ class RefsContainer:
         Args:
         Args:
           name: Name of the ref to set
           name: Name of the ref to set
           other: Name of the ref to point at
           other: Name of the ref to point at
+          committer: Optional committer name/email
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Optional message
           message: Optional message
         """
         """
         raise NotImplementedError(self.set_symbolic_ref)
         raise NotImplementedError(self.set_symbolic_ref)
@@ -213,6 +227,17 @@ class RefsContainer:
         message: Optional[bytes] = None,
         message: Optional[bytes] = None,
         prune: bool = False,
         prune: bool = False,
     ) -> None:
     ) -> None:
+        """Import refs from another repository.
+
+        Args:
+          base: Base ref to import into (e.g., b'refs/remotes/origin')
+          other: Dictionary of refs to import
+          committer: Optional committer for reflog
+          timestamp: Optional timestamp for reflog
+          timezone: Optional timezone for reflog
+          message: Optional message for reflog
+          prune: If True, remove refs not in other
+        """
         if prune:
         if prune:
             to_delete = set(self.subkeys(base))
             to_delete = set(self.subkeys(base))
         else:
         else:
@@ -237,6 +262,7 @@ class RefsContainer:
         raise NotImplementedError(self.allkeys)
         raise NotImplementedError(self.allkeys)
 
 
     def __iter__(self):
     def __iter__(self):
+        """Iterate over all ref names."""
         return iter(self.allkeys())
         return iter(self.allkeys())
 
 
     def keys(self, base=None):
     def keys(self, base=None):
@@ -346,6 +372,14 @@ class RefsContainer:
         return refnames, contents
         return refnames, contents
 
 
     def __contains__(self, refname) -> bool:
     def __contains__(self, refname) -> bool:
+        """Check if a ref exists.
+
+        Args:
+          refname: Name of the ref to check
+
+        Returns:
+          True if the ref exists
+        """
         if self.read_ref(refname):
         if self.read_ref(refname):
             return True
             return True
         return False
         return False
@@ -381,6 +415,9 @@ class RefsContainer:
           old_ref: The old sha the refname must refer to, or None to set
           old_ref: The old sha the refname must refer to, or None to set
             unconditionally.
             unconditionally.
           new_ref: The new sha the refname will refer to.
           new_ref: The new sha the refname will refer to.
+          committer: Optional committer name/email
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Message for reflog
           message: Message for reflog
         Returns: True if the set was successful, False otherwise.
         Returns: True if the set was successful, False otherwise.
         """
         """
@@ -394,6 +431,10 @@ class RefsContainer:
         Args:
         Args:
           name: Ref name
           name: Ref name
           ref: Ref value
           ref: Ref value
+          committer: Optional committer name/email
+          timestamp: Optional timestamp
+          timezone: Optional timezone
+          message: Optional message for reflog
         """
         """
         raise NotImplementedError(self.add_if_new)
         raise NotImplementedError(self.add_if_new)
 
 
@@ -434,6 +475,9 @@ class RefsContainer:
           name: The refname to delete.
           name: The refname to delete.
           old_ref: The old sha the refname must refer to, or None to
           old_ref: The old sha the refname must refer to, or None to
             delete unconditionally.
             delete unconditionally.
+          committer: Optional committer name/email
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Message for reflog
           message: Message for reflog
         Returns: True if the delete was successful, False otherwise.
         Returns: True if the delete was successful, False otherwise.
         """
         """
@@ -486,18 +530,37 @@ class DictRefsContainer(RefsContainer):
     """
     """
 
 
     def __init__(self, refs, logger=None) -> None:
     def __init__(self, refs, logger=None) -> None:
+        """Initialize DictRefsContainer."""
         super().__init__(logger=logger)
         super().__init__(logger=logger)
         self._refs = refs
         self._refs = refs
         self._peeled: dict[bytes, ObjectID] = {}
         self._peeled: dict[bytes, ObjectID] = {}
         self._watchers: set[Any] = set()
         self._watchers: set[Any] = set()
 
 
     def allkeys(self):
     def allkeys(self):
+        """Get all ref names.
+
+        Returns:
+          All ref names in the container
+        """
         return self._refs.keys()
         return self._refs.keys()
 
 
     def read_loose_ref(self, name):
     def read_loose_ref(self, name):
+        """Read a reference from the refs dictionary.
+
+        Args:
+          name: The ref name to read
+
+        Returns:
+          The ref value or None if not found
+        """
         return self._refs.get(name, None)
         return self._refs.get(name, None)
 
 
     def get_packed_refs(self):
     def get_packed_refs(self):
+        """Get packed refs (always empty for DictRefsContainer).
+
+        Returns:
+          Empty dictionary
+        """
         return {}
         return {}
 
 
     def _notify(self, ref, newsha) -> None:
     def _notify(self, ref, newsha) -> None:
@@ -513,6 +576,16 @@ class DictRefsContainer(RefsContainer):
         timezone=None,
         timezone=None,
         message=None,
         message=None,
     ) -> None:
     ) -> None:
+        """Make a ref point at another ref.
+
+        Args:
+          name: Name of the ref to set
+          other: Name of the ref to point at
+          committer: Optional committer name for reflog
+          timestamp: Optional timestamp for reflog
+          timezone: Optional timezone for reflog
+          message: Optional message for reflog
+        """
         old = self.follow(name)[-1]
         old = self.follow(name)[-1]
         new = SYMREF + other
         new = SYMREF + other
         self._refs[name] = new
         self._refs[name] = new
@@ -537,6 +610,24 @@ class DictRefsContainer(RefsContainer):
         timezone=None,
         timezone=None,
         message=None,
         message=None,
     ) -> bool:
     ) -> bool:
+        """Set a refname to new_ref only if it currently equals old_ref.
+
+        This method follows all symbolic references, and can be used to perform
+        an atomic compare-and-swap operation.
+
+        Args:
+          name: The refname to set.
+          old_ref: The old sha the refname must refer to, or None to set
+            unconditionally.
+          new_ref: The new sha the refname will refer to.
+          committer: Optional committer name for reflog
+          timestamp: Optional timestamp for reflog
+          timezone: Optional timezone for reflog
+          message: Optional message for reflog
+
+        Returns:
+          True if the set was successful, False otherwise.
+        """
         if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref:
         if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref:
             return False
             return False
         # Only update the specific ref requested, not the whole chain
         # Only update the specific ref requested, not the whole chain
@@ -564,6 +655,19 @@ class DictRefsContainer(RefsContainer):
         timezone=None,
         timezone=None,
         message: Optional[bytes] = None,
         message: Optional[bytes] = None,
     ) -> bool:
     ) -> bool:
+        """Add a new reference only if it does not already exist.
+
+        Args:
+          name: Ref name
+          ref: Ref value
+          committer: Optional committer name for reflog
+          timestamp: Optional timestamp for reflog
+          timezone: Optional timezone for reflog
+          message: Optional message for reflog
+
+        Returns:
+          True if the add was successful, False otherwise.
+        """
         if name in self._refs:
         if name in self._refs:
             return False
             return False
         self._refs[name] = ref
         self._refs[name] = ref
@@ -588,6 +692,23 @@ class DictRefsContainer(RefsContainer):
         timezone=None,
         timezone=None,
         message=None,
         message=None,
     ) -> bool:
     ) -> bool:
+        """Remove a refname only if it currently equals old_ref.
+
+        This method does not follow symbolic references. It can be used to
+        perform an atomic compare-and-delete operation.
+
+        Args:
+          name: The refname to delete.
+          old_ref: The old sha the refname must refer to, or None to
+            delete unconditionally.
+          committer: Optional committer name for reflog
+          timestamp: Optional timestamp for reflog
+          timezone: Optional timezone for reflog
+          message: Optional message for reflog
+
+        Returns:
+          True if the delete was successful, False otherwise.
+        """
         if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref:
         if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref:
             return False
             return False
         try:
         try:
@@ -608,6 +729,14 @@ class DictRefsContainer(RefsContainer):
         return True
         return True
 
 
     def get_peeled(self, name):
     def get_peeled(self, name):
+        """Get the peeled value of a ref.
+
+        Args:
+          name: Ref name to get peeled value for
+
+        Returns:
+          The peeled SHA or None if not available
+        """
         return self._peeled.get(name)
         return self._peeled.get(name)
 
 
     def _update(self, refs) -> None:
     def _update(self, refs) -> None:
@@ -626,21 +755,55 @@ class InfoRefsContainer(RefsContainer):
     """Refs container that reads refs from a info/refs file."""
     """Refs container that reads refs from a info/refs file."""
 
 
     def __init__(self, f) -> None:
     def __init__(self, f) -> None:
+        """Initialize an InfoRefsContainer.
+
+        Args:
+          f: File-like object containing info/refs data
+        """
         self._refs = {}
         self._refs = {}
         self._peeled = {}
         self._peeled = {}
         refs = read_info_refs(f)
         refs = read_info_refs(f)
         (self._refs, self._peeled) = split_peeled_refs(refs)
         (self._refs, self._peeled) = split_peeled_refs(refs)
 
 
     def allkeys(self):
     def allkeys(self):
+        """Get all ref names.
+
+        Returns:
+          All ref names in the info/refs file
+        """
         return self._refs.keys()
         return self._refs.keys()
 
 
     def read_loose_ref(self, name):
     def read_loose_ref(self, name):
+        """Read a reference from the parsed info/refs.
+
+        Args:
+          name: The ref name to read
+
+        Returns:
+          The ref value or None if not found
+        """
         return self._refs.get(name, None)
         return self._refs.get(name, None)
 
 
     def get_packed_refs(self):
     def get_packed_refs(self):
+        """Get packed refs (always empty for InfoRefsContainer).
+
+        Returns:
+          Empty dictionary
+        """
         return {}
         return {}
 
 
     def get_peeled(self, name):
     def get_peeled(self, name):
+        """Get the peeled value of a ref.
+
+        Args:
+          name: Ref name to get peeled value for
+
+        Returns:
+          The peeled SHA if available, otherwise the ref value itself
+
+        Raises:
+          KeyError: If the ref doesn't exist
+        """
         try:
         try:
             return self._peeled[name]
             return self._peeled[name]
         except KeyError:
         except KeyError:
@@ -656,6 +819,7 @@ class DiskRefsContainer(RefsContainer):
         worktree_path: Optional[Union[str, bytes, os.PathLike]] = None,
         worktree_path: Optional[Union[str, bytes, os.PathLike]] = None,
         logger=None,
         logger=None,
     ) -> None:
     ) -> None:
+        """Initialize DiskRefsContainer."""
         super().__init__(logger=logger)
         super().__init__(logger=logger)
         # Convert path-like objects to strings, then to bytes for Git compatibility
         # Convert path-like objects to strings, then to bytes for Git compatibility
         self.path = os.fsencode(os.fspath(path))
         self.path = os.fsencode(os.fspath(path))
@@ -667,9 +831,18 @@ class DiskRefsContainer(RefsContainer):
         self._peeled_refs = None
         self._peeled_refs = None
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of DiskRefsContainer."""
         return f"{self.__class__.__name__}({self.path!r})"
         return f"{self.__class__.__name__}({self.path!r})"
 
 
     def subkeys(self, base):
     def subkeys(self, base):
+        """Get all ref names under a base ref.
+
+        Args:
+          base: Base ref path to search under
+
+        Returns:
+          Set of ref names under the base (without base prefix)
+        """
         subkeys = set()
         subkeys = set()
         path = self.refpath(base)
         path = self.refpath(base)
         for root, unused_dirs, files in os.walk(path):
         for root, unused_dirs, files in os.walk(path):
@@ -689,6 +862,11 @@ class DiskRefsContainer(RefsContainer):
         return subkeys
         return subkeys
 
 
     def allkeys(self):
     def allkeys(self):
+        """Get all ref names from disk.
+
+        Returns:
+          Set of all ref names (both loose and packed)
+        """
         allkeys = set()
         allkeys = set()
         if os.path.exists(self.refpath(HEADREF)):
         if os.path.exists(self.refpath(HEADREF)):
             allkeys.add(HEADREF)
             allkeys.add(HEADREF)
@@ -870,6 +1048,9 @@ class DiskRefsContainer(RefsContainer):
         Args:
         Args:
           name: Name of the ref to set
           name: Name of the ref to set
           other: Name of the ref to point at
           other: Name of the ref to point at
+          committer: Optional committer name
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Optional message to describe the change
           message: Optional message to describe the change
         """
         """
         self._check_refname(name)
         self._check_refname(name)
@@ -914,6 +1095,9 @@ class DiskRefsContainer(RefsContainer):
           old_ref: The old sha the refname must refer to, or None to set
           old_ref: The old sha the refname must refer to, or None to set
             unconditionally.
             unconditionally.
           new_ref: The new sha the refname will refer to.
           new_ref: The new sha the refname will refer to.
+          committer: Optional committer name
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Set message for reflog
           message: Set message for reflog
         Returns: True if the set was successful, False otherwise.
         Returns: True if the set was successful, False otherwise.
         """
         """
@@ -992,6 +1176,9 @@ class DiskRefsContainer(RefsContainer):
         Args:
         Args:
           name: The refname to set.
           name: The refname to set.
           ref: The new sha the refname will refer to.
           ref: The new sha the refname will refer to.
+          committer: Optional committer name
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Optional message for reflog
           message: Optional message for reflog
         Returns: True if the add was successful, False otherwise.
         Returns: True if the add was successful, False otherwise.
         """
         """
@@ -1044,6 +1231,9 @@ class DiskRefsContainer(RefsContainer):
           name: The refname to delete.
           name: The refname to delete.
           old_ref: The old sha the refname must refer to, or None to
           old_ref: The old sha the refname must refer to, or None to
             delete unconditionally.
             delete unconditionally.
+          committer: Optional committer name
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Optional message
           message: Optional message
         Returns: True if the delete was successful, False otherwise.
         Returns: True if the delete was successful, False otherwise.
         """
         """
@@ -1211,6 +1401,14 @@ def write_packed_refs(f, packed_refs, peeled_refs=None) -> None:
 
 
 
 
 def read_info_refs(f):
 def read_info_refs(f):
+    """Read info/refs file.
+
+    Args:
+      f: File-like object to read from
+
+    Returns:
+      Dictionary mapping ref names to SHA1s
+    """
     ret = {}
     ret = {}
     for line in f.readlines():
     for line in f.readlines():
         (sha, name) = line.rstrip(b"\r\n").split(b"\t", 1)
         (sha, name) = line.rstrip(b"\r\n").split(b"\t", 1)
@@ -1239,6 +1437,14 @@ def write_info_refs(refs, store: ObjectContainer):
 
 
 
 
 def is_local_branch(x):
 def is_local_branch(x):
+    """Check if a ref name refers to a local branch.
+
+    Args:
+      x: Ref name to check
+
+    Returns:
+      True if ref is a local branch (refs/heads/...)
+    """
     return x.startswith(LOCAL_BRANCH_PREFIX)
     return x.startswith(LOCAL_BRANCH_PREFIX)
 
 
 
 
@@ -1356,6 +1562,15 @@ def _import_remote_refs(
 
 
 
 
 def serialize_refs(store, refs):
 def serialize_refs(store, refs):
+    """Serialize refs with peeled refs.
+
+    Args:
+      store: Object store to peel refs from
+      refs: Dictionary of ref names to SHAs
+
+    Returns:
+      Dictionary with refs and peeled refs (marked with ^{})
+    """
     # TODO: Avoid recursive import :(
     # TODO: Avoid recursive import :(
     from .object_store import peel_sha
     from .object_store import peel_sha
 
 
@@ -1385,6 +1600,12 @@ class locked_ref:
     """
     """
 
 
     def __init__(self, refs_container: DiskRefsContainer, refname: Ref) -> None:
     def __init__(self, refs_container: DiskRefsContainer, refname: Ref) -> None:
+        """Initialize a locked ref.
+
+        Args:
+          refs_container: The DiskRefsContainer to lock the ref in
+          refname: The ref name to lock
+        """
         self._refs_container = refs_container
         self._refs_container = refs_container
         self._refname = refname
         self._refname = refname
         self._file: Optional[_GitFile] = None
         self._file: Optional[_GitFile] = None
@@ -1392,6 +1613,14 @@ class locked_ref:
         self._deleted = False
         self._deleted = False
 
 
     def __enter__(self) -> "locked_ref":
     def __enter__(self) -> "locked_ref":
+        """Enter the context manager and acquire the lock.
+
+        Returns:
+          This locked_ref instance
+
+        Raises:
+          OSError: If the lock cannot be acquired
+        """
         self._refs_container._check_refname(self._refname)
         self._refs_container._check_refname(self._refname)
         try:
         try:
             realnames, _ = self._refs_container.follow(self._refname)
             realnames, _ = self._refs_container.follow(self._refname)
@@ -1411,6 +1640,13 @@ class locked_ref:
         exc_value: Optional[BaseException],
         exc_value: Optional[BaseException],
         traceback: Optional[types.TracebackType],
         traceback: Optional[types.TracebackType],
     ) -> None:
     ) -> None:
+        """Exit the context manager and release the lock.
+
+        Args:
+          exc_type: Type of exception if one occurred
+          exc_value: Exception instance if one occurred
+          traceback: Traceback if an exception occurred
+        """
         if self._file:
         if self._file:
             if exc_type is not None or self._deleted:
             if exc_type is not None or self._deleted:
                 self._file.abort()
                 self._file.abort()

+ 28 - 0
dulwich/reftable.py

@@ -225,6 +225,13 @@ class RefUpdate:
     """A reference update operation."""
     """A reference update operation."""
 
 
     def __init__(self, name: bytes, value_type: int, value: bytes):
     def __init__(self, name: bytes, value_type: int, value: bytes):
+        """Initialize RefUpdate.
+
+        Args:
+          name: Reference name
+          value_type: Type of reference value
+          value: Reference value
+        """
         self.name = name
         self.name = name
         self.value_type = value_type
         self.value_type = value_type
         self.value = value
         self.value = value
@@ -236,6 +243,14 @@ class RefRecord:
     def __init__(
     def __init__(
         self, refname: bytes, value_type: int, value: bytes, update_index: int = 0
         self, refname: bytes, value_type: int, value: bytes, update_index: int = 0
     ):
     ):
+        """Initialize RefRecord.
+
+        Args:
+          refname: Reference name
+          value_type: Type of reference value
+          value: Reference value
+          update_index: Update index for the reference
+        """
         self.refname = refname
         self.refname = refname
         self.value_type = value_type
         self.value_type = value_type
         self.value = value
         self.value = value
@@ -323,6 +338,7 @@ class RefBlock:
     """A block containing reference records."""
     """A block containing reference records."""
 
 
     def __init__(self):
     def __init__(self):
+        """Initialize RefBlock."""
         self.refs = []
         self.refs = []
 
 
     def add_ref(
     def add_ref(
@@ -478,6 +494,13 @@ class ReftableWriter:
         auto_create_head: bool = True,
         auto_create_head: bool = True,
         is_batch_operation: bool = False,
         is_batch_operation: bool = False,
     ):
     ):
+        """Initialize ReftableWriter.
+
+        Args:
+          f: Binary file object to write to
+          auto_create_head: Whether to automatically create HEAD reference
+          is_batch_operation: Whether this is a batch operation
+        """
         self.f = f
         self.f = f
         self.refs: dict[bytes, tuple[int, bytes]] = {}
         self.refs: dict[bytes, tuple[int, bytes]] = {}
         self.refs_order: list[bytes] = []  # Track insertion order for update indices
         self.refs_order: list[bytes] = []  # Track insertion order for update indices
@@ -662,6 +685,11 @@ class ReftableReader:
     """Reader for reftable files."""
     """Reader for reftable files."""
 
 
     def __init__(self, f: BinaryIO):
     def __init__(self, f: BinaryIO):
+        """Initialize ReftableReader.
+
+        Args:
+          f: Binary file object to read from
+        """
         self.f = f
         self.f = f
         self._read_header()
         self._read_header()
         self.refs: dict[bytes, tuple[int, bytes]] = {}
         self.refs: dict[bytes, tuple[int, bytes]] = {}

+ 118 - 21
dulwich/repo.py

@@ -139,6 +139,11 @@ class InvalidUserIdentity(Exception):
     """User identity is not of the format 'user <email>'."""
     """User identity is not of the format 'user <email>'."""
 
 
     def __init__(self, identity) -> None:
     def __init__(self, identity) -> None:
+        """Initialize InvalidUserIdentity exception.
+
+        Args:
+            identity: The invalid identity string
+        """
         self.identity = identity
         self.identity = identity
 
 
 
 
@@ -200,6 +205,7 @@ def get_user_identity(config: "StackedConfig", kind: Optional[str] = None) -> by
     system (e.g. the gecos field, $EMAIL, $USER@$(hostname -f).
     system (e.g. the gecos field, $EMAIL, $USER@$(hostname -f).
 
 
     Args:
     Args:
+      config: Configuration stack to read from
       kind: Optional kind to return identity for,
       kind: Optional kind to return identity for,
         usually either "AUTHOR" or "COMMITTER".
         usually either "AUTHOR" or "COMMITTER".
 
 
@@ -331,7 +337,16 @@ def _set_filesystem_hidden(path) -> None:
 
 
 
 
 class ParentsProvider:
 class ParentsProvider:
+    """Provides parents for commits, handling grafts and shallow commits."""
+
     def __init__(self, store, grafts={}, shallows=[]) -> None:
     def __init__(self, store, grafts={}, shallows=[]) -> None:
+        """Initialize ParentsProvider.
+
+        Args:
+            store: Object store to get commits from
+            grafts: Dictionary mapping commit ids to parent ids
+            shallows: List of shallow commit ids
+        """
         self.store = store
         self.store = store
         self.grafts = grafts
         self.grafts = grafts
         self.shallows = set(shallows)
         self.shallows = set(shallows)
@@ -340,6 +355,15 @@ class ParentsProvider:
         self.commit_graph = store.get_commit_graph()
         self.commit_graph = store.get_commit_graph()
 
 
     def get_parents(self, commit_id, commit=None):
     def get_parents(self, commit_id, commit=None):
+        """Get the parents of a commit.
+
+        Args:
+          commit_id: The commit SHA to get parents for
+          commit: Optional commit object to avoid fetching
+
+        Returns:
+          List of parent commit SHAs
+        """
         try:
         try:
             return self.grafts[commit_id]
             return self.grafts[commit_id]
         except KeyError:
         except KeyError:
@@ -581,7 +605,14 @@ class BaseRepo:
                 return None
                 return None
 
 
             class DummyMissingObjectFinder:
             class DummyMissingObjectFinder:
+                """Dummy finder that returns no missing objects."""
+
                 def get_remote_has(self) -> None:
                 def get_remote_has(self) -> None:
+                    """Get remote has (always returns None).
+
+                    Returns:
+                      None
+                    """
                     return None
                     return None
 
 
                 def __len__(self) -> int:
                 def __len__(self) -> int:
@@ -607,6 +638,14 @@ class BaseRepo:
         parents_provider = ParentsProvider(self.object_store, shallows=current_shallow)
         parents_provider = ParentsProvider(self.object_store, shallows=current_shallow)
 
 
         def get_parents(commit):
         def get_parents(commit):
+            """Get parents for a commit using the parents provider.
+
+            Args:
+              commit: Commit object
+
+            Returns:
+              List of parent commit SHAs
+            """
             return parents_provider.get_parents(commit.id, commit)
             return parents_provider.get_parents(commit.id, commit)
 
 
         return MissingObjectFinder(
         return MissingObjectFinder(
@@ -708,6 +747,11 @@ class BaseRepo:
         return self.object_store[sha]
         return self.object_store[sha]
 
 
     def parents_provider(self) -> ParentsProvider:
     def parents_provider(self) -> ParentsProvider:
+        """Get a parents provider for this repository.
+
+        Returns:
+          ParentsProvider instance configured with grafts and shallows
+        """
         return ParentsProvider(
         return ParentsProvider(
             self.object_store,
             self.object_store,
             grafts=self._graftpoints,
             grafts=self._graftpoints,
@@ -857,26 +901,25 @@ class BaseRepo:
         Args:
         Args:
           include: Iterable of SHAs of commits to include along with their
           include: Iterable of SHAs of commits to include along with their
             ancestors. Defaults to [HEAD]
             ancestors. Defaults to [HEAD]
-
-        Keyword Args:
-          exclude: Iterable of SHAs of commits to exclude along with their
-            ancestors, overriding includes.
-          order: ORDER_* constant specifying the order of results.
-            Anything other than ORDER_DATE may result in O(n) memory usage.
-          reverse: If True, reverse the order of output, requiring O(n)
-            memory.
-          max_entries: The maximum number of entries to yield, or None for
-            no limit.
-          paths: Iterable of file or subtree paths to show entries for.
-          rename_detector: diff.RenameDetector object for detecting
-            renames.
-          follow: If True, follow path across renames/copies. Forces a
-            default rename_detector.
-          since: Timestamp to list commits after.
-          until: Timestamp to list commits before.
-          queue_cls: A class to use for a queue of commits, supporting the
-            iterator protocol. The constructor takes a single argument, the
-            Walker.
+          **kwargs: Additional keyword arguments including:
+
+            * exclude: Iterable of SHAs of commits to exclude along with their
+              ancestors, overriding includes.
+            * order: ORDER_* constant specifying the order of results.
+              Anything other than ORDER_DATE may result in O(n) memory usage.
+            * reverse: If True, reverse the order of output, requiring O(n)
+              memory.
+            * max_entries: The maximum number of entries to yield, or None for
+              no limit.
+            * paths: Iterable of file or subtree paths to show entries for.
+            * rename_detector: diff.RenameDetector object for detecting
+              renames.
+            * follow: If True, follow path across renames/copies. Forces a
+              default rename_detector.
+            * since: Timestamp to list commits after.
+            * until: Timestamp to list commits before.
+            * queue_cls: A class to use for a queue of commits, supporting the
+              iterator protocol. The constructor takes a single argument, the Walker.
 
 
         Returns: A `Walker` object
         Returns: A `Walker` object
         """
         """
@@ -1085,6 +1128,11 @@ class UnsupportedVersion(Exception):
     """Unsupported repository version."""
     """Unsupported repository version."""
 
 
     def __init__(self, version) -> None:
     def __init__(self, version) -> None:
+        """Initialize UnsupportedVersion exception.
+
+        Args:
+            version: The unsupported repository version
+        """
         self.version = version
         self.version = version
 
 
 
 
@@ -1092,6 +1140,11 @@ class UnsupportedExtension(Exception):
     """Unsupported repository extension."""
     """Unsupported repository extension."""
 
 
     def __init__(self, extension) -> None:
     def __init__(self, extension) -> None:
+        """Initialize UnsupportedExtension exception.
+
+        Args:
+            extension: The unsupported repository extension
+        """
         self.extension = extension
         self.extension = extension
 
 
 
 
@@ -1459,7 +1512,8 @@ class Repo(BaseRepo):
 
 
     @replace_me(remove_in="0.26.0")
     @replace_me(remove_in="0.26.0")
     def unstage(self, fs_paths: list[str]) -> None:
     def unstage(self, fs_paths: list[str]) -> None:
-        """Unstage specific file in the index
+        """Unstage specific file in the index.
+
         Args:
         Args:
           fs_paths: a list of files to unstage,
           fs_paths: a list of files to unstage,
             relative to the repository path.
             relative to the repository path.
@@ -1581,6 +1635,15 @@ class Repo(BaseRepo):
 
 
         # Add gitdir matchers
         # Add gitdir matchers
         def match_gitdir(pattern: str, case_sensitive: bool = True) -> bool:
         def match_gitdir(pattern: str, case_sensitive: bool = True) -> bool:
+            """Match gitdir against a pattern.
+
+            Args:
+              pattern: Pattern to match against
+              case_sensitive: Whether to match case-sensitively
+
+            Returns:
+              True if gitdir matches pattern
+            """
             # Handle relative patterns (starting with ./)
             # Handle relative patterns (starting with ./)
             if pattern.startswith("./"):
             if pattern.startswith("./"):
                 # Can't handle relative patterns without config directory context
                 # Can't handle relative patterns without config directory context
@@ -1618,6 +1681,14 @@ class Repo(BaseRepo):
 
 
         # Add onbranch matcher
         # Add onbranch matcher
         def match_onbranch(pattern: str) -> bool:
         def match_onbranch(pattern: str) -> bool:
+            """Match current branch against a pattern.
+
+            Args:
+              pattern: Pattern to match against
+
+            Returns:
+              True if current branch matches pattern
+            """
             try:
             try:
                 # Get the current branch using refs
                 # Get the current branch using refs
                 ref_chain, _ = self.refs.follow(b"HEAD")
                 ref_chain, _ = self.refs.follow(b"HEAD")
@@ -1640,6 +1711,11 @@ class Repo(BaseRepo):
         return matchers
         return matchers
 
 
     def get_worktree_config(self) -> "ConfigFile":
     def get_worktree_config(self) -> "ConfigFile":
+        """Get the worktree-specific config.
+
+        Returns:
+          ConfigFile object for the worktree config
+        """
         from .config import ConfigFile
         from .config import ConfigFile
 
 
         path = os.path.join(self.commondir(), "config.worktree")
         path = os.path.join(self.commondir(), "config.worktree")
@@ -1694,6 +1770,7 @@ class Repo(BaseRepo):
             return None
             return None
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of this repository."""
         return f"<Repo at {self.path!r}>"
         return f"<Repo at {self.path!r}>"
 
 
     def set_description(self, description) -> None:
     def set_description(self, description) -> None:
@@ -1756,6 +1833,9 @@ class Repo(BaseRepo):
         Args:
         Args:
           path: Path in which to create the repository
           path: Path in which to create the repository
           mkdir: Whether to create the directory
           mkdir: Whether to create the directory
+          config: Configuration object
+          default_branch: Default branch name
+          symlinks: Whether to support symlinks
           format: Repository format version (defaults to 0)
           format: Repository format version (defaults to 0)
         Returns: `Repo` instance
         Returns: `Repo` instance
         """
         """
@@ -1843,6 +1923,10 @@ class Repo(BaseRepo):
 
 
         Args:
         Args:
           path: Path to create bare repository in
           path: Path to create bare repository in
+          mkdir: Whether to create the directory
+          object_store: Object store to use
+          config: Configuration object
+          default_branch: Default branch name
           format: Repository format version (defaults to 0)
           format: Repository format version (defaults to 0)
         Returns: a `Repo` instance
         Returns: a `Repo` instance
         """
         """
@@ -1868,9 +1952,11 @@ class Repo(BaseRepo):
         self.object_store.close()
         self.object_store.close()
 
 
     def __enter__(self):
     def __enter__(self):
+        """Enter context manager."""
         return self
         return self
 
 
     def __exit__(self, exc_type, exc_val, exc_tb):
     def __exit__(self, exc_type, exc_val, exc_tb):
+        """Exit context manager and close repository."""
         self.close()
         self.close()
 
 
     def _read_gitattributes(self) -> dict[bytes, dict[bytes, bytes]]:
     def _read_gitattributes(self) -> dict[bytes, dict[bytes, bytes]]:
@@ -2060,9 +2146,19 @@ class MemoryRepo(BaseRepo):
         self._reflog.append(args)
         self._reflog.append(args)
 
 
     def set_description(self, description) -> None:
     def set_description(self, description) -> None:
+        """Set the description for this repository.
+
+        Args:
+          description: Text to set as description
+        """
         self._description = description
         self._description = description
 
 
     def get_description(self):
     def get_description(self):
+        """Get the description of this repository.
+
+        Returns:
+          Repository description as bytes
+        """
         return self._description
         return self._description
 
 
     def _determine_file_mode(self):
     def _determine_file_mode(self):
@@ -2103,6 +2199,7 @@ class MemoryRepo(BaseRepo):
 
 
         Args:
         Args:
           path: The path to the file, relative to the control dir.
           path: The path to the file, relative to the control dir.
+          basedir: Optional base directory for the path
         Returns: An open file object, or None if the file does not exist.
         Returns: An open file object, or None if the file does not exist.
         """
         """
         contents = self._named_files.get(path, None)
         contents = self._named_files.get(path, None)

+ 181 - 0
dulwich/server.py

@@ -172,6 +172,8 @@ class BackendRepo(TypingProtocol):
         """Yield the objects required for a list of commits.
         """Yield the objects required for a list of commits.
 
 
         Args:
         Args:
+          determine_wants: Function to determine which objects the client wants
+          graph_walker: Object used to walk the commit graph
           progress: is a callback to send progress messages to the client
           progress: is a callback to send progress messages to the client
           get_tagged: Function that returns a dict of pointed-to sha ->
           get_tagged: Function that returns a dict of pointed-to sha ->
             tag sha for including tags.
             tag sha for including tags.
@@ -191,6 +193,17 @@ class DictBackend(Backend):
         self.repos = repos
         self.repos = repos
 
 
     def open_repository(self, path: str) -> BackendRepo:
     def open_repository(self, path: str) -> BackendRepo:
+        """Open repository at given path.
+
+        Args:
+          path: Path to the repository
+
+        Returns:
+          Repository object
+
+        Raises:
+          NotGitRepository: If no repository found at path
+        """
         logger.debug("Opening repository at %s", path)
         logger.debug("Opening repository at %s", path)
         try:
         try:
             return self.repos[path]
             return self.repos[path]
@@ -277,6 +290,11 @@ class PackHandler(Handler):
 
 
     @classmethod
     @classmethod
     def innocuous_capabilities(cls) -> Iterable[bytes]:
     def innocuous_capabilities(cls) -> Iterable[bytes]:
+        """Return capabilities that don't affect protocol behavior.
+
+        Returns:
+            List of innocuous capability names
+        """
         return [
         return [
             CAPABILITY_INCLUDE_TAG,
             CAPABILITY_INCLUDE_TAG,
             CAPABILITY_THIN_PACK,
             CAPABILITY_THIN_PACK,
@@ -600,6 +618,11 @@ class AckGraphWalkerImpl:
     """Base class for acknowledgment graph walker implementations."""
     """Base class for acknowledgment graph walker implementations."""
 
 
     def __init__(self, graph_walker):
     def __init__(self, graph_walker):
+        """Initialize acknowledgment graph walker.
+
+        Args:
+            graph_walker: Graph walker to wrap
+        """
         raise NotImplementedError
         raise NotImplementedError
 
 
     def ack(self, have_ref: ObjectID) -> None:
     def ack(self, have_ref: ObjectID) -> None:
@@ -668,6 +691,8 @@ class _ProtocolGraphWalker:
 
 
         Args:
         Args:
           heads: a dict of refname->SHA1 to advertise
           heads: a dict of refname->SHA1 to advertise
+          depth: Maximum depth for shallow clones
+
         Returns: a list of SHA1s requested by the client
         Returns: a list of SHA1s requested by the client
         """
         """
         symrefs = self.get_symrefs()
         symrefs = self.get_symrefs()
@@ -909,11 +934,21 @@ class SingleAckGraphWalkerImpl(AckGraphWalkerImpl):
         self._common: list[bytes] = []
         self._common: list[bytes] = []
 
 
     def ack(self, have_ref) -> None:
     def ack(self, have_ref) -> None:
+        """Acknowledge a have reference.
+
+        Args:
+            have_ref: Object ID to acknowledge
+        """
         if not self._common:
         if not self._common:
             self.walker.send_ack(have_ref)
             self.walker.send_ack(have_ref)
             self._common.append(have_ref)
             self._common.append(have_ref)
 
 
     def next(self):
     def next(self):
+        """Get next SHA from graph walker.
+
+        Returns:
+            SHA bytes or None if done
+        """
         command, sha = self.walker.read_proto_line(_GRAPH_WALKER_COMMANDS)
         command, sha = self.walker.read_proto_line(_GRAPH_WALKER_COMMANDS)
         if command in (None, COMMAND_DONE):
         if command in (None, COMMAND_DONE):
             # defer the handling of done
             # defer the handling of done
@@ -925,6 +960,15 @@ class SingleAckGraphWalkerImpl(AckGraphWalkerImpl):
     __next__ = next
     __next__ = next
 
 
     def handle_done(self, done_required, done_received) -> bool:
     def handle_done(self, done_required, done_received) -> bool:
+        """Handle done command.
+
+        Args:
+            done_required: Whether done is required
+            done_received: Whether done was received
+
+        Returns:
+            True if handling completed successfully
+        """
         if not self._common:
         if not self._common:
             self.walker.send_nak()
             self.walker.send_nak()
 
 
@@ -949,11 +993,21 @@ class MultiAckGraphWalkerImpl(AckGraphWalkerImpl):
     """Graph walker implementation that speaks the multi-ack protocol."""
     """Graph walker implementation that speaks the multi-ack protocol."""
 
 
     def __init__(self, walker) -> None:
     def __init__(self, walker) -> None:
+        """Initialize multi-ack graph walker.
+
+        Args:
+            walker: Parent ProtocolGraphWalker instance
+        """
         self.walker = walker
         self.walker = walker
         self._found_base = False
         self._found_base = False
         self._common: list[bytes] = []
         self._common: list[bytes] = []
 
 
     def ack(self, have_ref) -> None:
     def ack(self, have_ref) -> None:
+        """Acknowledge a have reference.
+
+        Args:
+            have_ref: Object ID to acknowledge
+        """
         self._common.append(have_ref)
         self._common.append(have_ref)
         if not self._found_base:
         if not self._found_base:
             self.walker.send_ack(have_ref, b"continue")
             self.walker.send_ack(have_ref, b"continue")
@@ -962,6 +1016,11 @@ class MultiAckGraphWalkerImpl(AckGraphWalkerImpl):
         # else we blind ack within next
         # else we blind ack within next
 
 
     def next(self):
     def next(self):
+        """Get next SHA from graph walker.
+
+        Returns:
+            SHA bytes or None if done
+        """
         while True:
         while True:
             command, sha = self.walker.read_proto_line(_GRAPH_WALKER_COMMANDS)
             command, sha = self.walker.read_proto_line(_GRAPH_WALKER_COMMANDS)
             if command is None:
             if command is None:
@@ -981,6 +1040,15 @@ class MultiAckGraphWalkerImpl(AckGraphWalkerImpl):
     __next__ = next
     __next__ = next
 
 
     def handle_done(self, done_required, done_received) -> bool:
     def handle_done(self, done_required, done_received) -> bool:
+        """Handle done command.
+
+        Args:
+            done_required: Whether done is required
+            done_received: Whether done was received
+
+        Returns:
+            True if handling completed successfully
+        """
         if done_required and not done_received:
         if done_required and not done_received:
             # we are not done, especially when done is required; skip
             # we are not done, especially when done is required; skip
             # the pack for this request and especially do not handle
             # the pack for this request and especially do not handle
@@ -1008,15 +1076,30 @@ class MultiAckDetailedGraphWalkerImpl(AckGraphWalkerImpl):
     """Graph walker implementation speaking the multi-ack-detailed protocol."""
     """Graph walker implementation speaking the multi-ack-detailed protocol."""
 
 
     def __init__(self, walker) -> None:
     def __init__(self, walker) -> None:
+        """Initialize multi-ack-detailed graph walker.
+
+        Args:
+            walker: Parent ProtocolGraphWalker instance
+        """
         self.walker = walker
         self.walker = walker
         self._common: list[bytes] = []
         self._common: list[bytes] = []
 
 
     def ack(self, have_ref) -> None:
     def ack(self, have_ref) -> None:
+        """Acknowledge a have reference.
+
+        Args:
+            have_ref: Object ID to acknowledge
+        """
         # Should only be called iff have_ref is common
         # Should only be called iff have_ref is common
         self._common.append(have_ref)
         self._common.append(have_ref)
         self.walker.send_ack(have_ref, b"common")
         self.walker.send_ack(have_ref, b"common")
 
 
     def next(self):
     def next(self):
+        """Get next SHA from graph walker.
+
+        Returns:
+            SHA bytes or None if done
+        """
         while True:
         while True:
             command, sha = self.walker.read_proto_line(_GRAPH_WALKER_COMMANDS)
             command, sha = self.walker.read_proto_line(_GRAPH_WALKER_COMMANDS)
             if command is None:
             if command is None:
@@ -1046,6 +1129,15 @@ class MultiAckDetailedGraphWalkerImpl(AckGraphWalkerImpl):
     __next__ = next
     __next__ = next
 
 
     def handle_done(self, done_required, done_received) -> bool:
     def handle_done(self, done_required, done_received) -> bool:
+        """Handle done command.
+
+        Args:
+            done_required: Whether done is required
+            done_received: Whether done was received
+
+        Returns:
+            True if handling completed successfully
+        """
         if done_required and not done_received:
         if done_required and not done_received:
             # we are not done, especially when done is required; skip
             # we are not done, especially when done is required; skip
             # the pack for this request and especially do not handle
             # the pack for this request and especially do not handle
@@ -1075,12 +1167,26 @@ class ReceivePackHandler(PackHandler):
     def __init__(
     def __init__(
         self, backend, args, proto, stateless_rpc=False, advertise_refs=False
         self, backend, args, proto, stateless_rpc=False, advertise_refs=False
     ) -> None:
     ) -> None:
+        """Initialize receive-pack handler.
+
+        Args:
+            backend: Backend instance
+            args: Command arguments
+            proto: Protocol instance
+            stateless_rpc: Whether to use stateless RPC
+            advertise_refs: Whether to advertise refs
+        """
         super().__init__(backend, proto, stateless_rpc=stateless_rpc)
         super().__init__(backend, proto, stateless_rpc=stateless_rpc)
         self.repo = backend.open_repository(args[0])
         self.repo = backend.open_repository(args[0])
         self.advertise_refs = advertise_refs
         self.advertise_refs = advertise_refs
 
 
     @classmethod
     @classmethod
     def capabilities(cls) -> Iterable[bytes]:
     def capabilities(cls) -> Iterable[bytes]:
+        """Return supported capabilities.
+
+        Returns:
+            List of capability names
+        """
         return [
         return [
             CAPABILITY_REPORT_STATUS,
             CAPABILITY_REPORT_STATUS,
             CAPABILITY_DELETE_REFS,
             CAPABILITY_DELETE_REFS,
@@ -1093,6 +1199,14 @@ class ReceivePackHandler(PackHandler):
     def _apply_pack(
     def _apply_pack(
         self, refs: list[tuple[ObjectID, ObjectID, Ref]]
         self, refs: list[tuple[ObjectID, ObjectID, Ref]]
     ) -> Iterator[tuple[bytes, bytes]]:
     ) -> Iterator[tuple[bytes, bytes]]:
+        """Apply received pack to repository.
+
+        Args:
+            refs: List of (old_sha, new_sha, ref_name) tuples
+
+        Yields:
+            Tuples of (ref_name, error_message) for any errors
+        """
         all_exceptions = (
         all_exceptions = (
             IOError,
             IOError,
             OSError,
             OSError,
@@ -1147,6 +1261,11 @@ class ReceivePackHandler(PackHandler):
             yield (ref, ref_status)
             yield (ref, ref_status)
 
 
     def _report_status(self, status: list[tuple[bytes, bytes]]) -> None:
     def _report_status(self, status: list[tuple[bytes, bytes]]) -> None:
+        """Report status to client.
+
+        Args:
+            status: List of (ref_name, status_message) tuples
+        """
         if self.has_capability(CAPABILITY_SIDE_BAND_64K):
         if self.has_capability(CAPABILITY_SIDE_BAND_64K):
             writer = BufferedPktLineWriter(
             writer = BufferedPktLineWriter(
                 lambda d: self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, d)
                 lambda d: self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, d)
@@ -1174,6 +1293,11 @@ class ReceivePackHandler(PackHandler):
         flush()
         flush()
 
 
     def _on_post_receive(self, client_refs) -> None:
     def _on_post_receive(self, client_refs) -> None:
+        """Run post-receive hook.
+
+        Args:
+            client_refs: Dictionary of ref changes from client
+        """
         hook = self.repo.hooks.get("post-receive", None)
         hook = self.repo.hooks.get("post-receive", None)
         if not hook:
         if not hook:
             return
             return
@@ -1185,6 +1309,7 @@ class ReceivePackHandler(PackHandler):
             self.proto.write_sideband(SIDE_BAND_CHANNEL_FATAL, str(err).encode("utf-8"))
             self.proto.write_sideband(SIDE_BAND_CHANNEL_FATAL, str(err).encode("utf-8"))
 
 
     def handle(self) -> None:
     def handle(self) -> None:
+        """Handle receive-pack request."""
         if self.advertise_refs or not self.stateless_rpc:
         if self.advertise_refs or not self.stateless_rpc:
             refs = sorted(self.repo.get_refs().items())
             refs = sorted(self.repo.get_refs().items())
             symrefs = sorted(self.repo.refs.get_symrefs().items())
             symrefs = sorted(self.repo.refs.get_symrefs().items())
@@ -1235,11 +1360,23 @@ class ReceivePackHandler(PackHandler):
 
 
 
 
 class UploadArchiveHandler(Handler):
 class UploadArchiveHandler(Handler):
+    """Handler for git-upload-archive requests."""
+
     def __init__(self, backend, args, proto, stateless_rpc=False) -> None:
     def __init__(self, backend, args, proto, stateless_rpc=False) -> None:
+        """Initialize upload-archive handler.
+
+        Args:
+            backend: Backend instance
+            args: Command arguments
+            proto: Protocol instance
+            stateless_rpc: Whether to use stateless RPC
+        """
         super().__init__(backend, proto, stateless_rpc)
         super().__init__(backend, proto, stateless_rpc)
         self.repo = backend.open_repository(args[0])
         self.repo = backend.open_repository(args[0])
 
 
     def handle(self) -> None:
     def handle(self) -> None:
+        """Handle upload-archive request."""
+
         def write(x):
         def write(x):
             return self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, x)
             return self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, x)
 
 
@@ -1287,11 +1424,21 @@ DEFAULT_HANDLERS = {
 
 
 
 
 class TCPGitRequestHandler(socketserver.StreamRequestHandler):
 class TCPGitRequestHandler(socketserver.StreamRequestHandler):
+    """TCP request handler for git protocol."""
+
     def __init__(self, handlers, *args, **kwargs) -> None:
     def __init__(self, handlers, *args, **kwargs) -> None:
+        """Initialize TCP request handler.
+
+        Args:
+            handlers: Dictionary mapping commands to handler classes
+            *args: Additional arguments for StreamRequestHandler
+            **kwargs: Additional keyword arguments for StreamRequestHandler
+        """
         self.handlers = handlers
         self.handlers = handlers
         socketserver.StreamRequestHandler.__init__(self, *args, **kwargs)
         socketserver.StreamRequestHandler.__init__(self, *args, **kwargs)
 
 
     def handle(self) -> None:
     def handle(self) -> None:
+        """Handle TCP git request."""
         proto = ReceivableProtocol(self.connection.recv, self.wfile.write)
         proto = ReceivableProtocol(self.connection.recv, self.wfile.write)
         command, args = proto.read_cmd()
         command, args = proto.read_cmd()
         logger.info("Handling %s request, args=%s", command, args)
         logger.info("Handling %s request, args=%s", command, args)
@@ -1304,13 +1451,32 @@ class TCPGitRequestHandler(socketserver.StreamRequestHandler):
 
 
 
 
 class TCPGitServer(socketserver.TCPServer):
 class TCPGitServer(socketserver.TCPServer):
+    """TCP server for git protocol."""
+
     allow_reuse_address = True
     allow_reuse_address = True
     serve = socketserver.TCPServer.serve_forever
     serve = socketserver.TCPServer.serve_forever
 
 
     def _make_handler(self, *args, **kwargs):
     def _make_handler(self, *args, **kwargs):
+        """Create request handler instance.
+
+        Args:
+            *args: Handler arguments
+            **kwargs: Handler keyword arguments
+
+        Returns:
+            TCPGitRequestHandler instance
+        """
         return TCPGitRequestHandler(self.handlers, *args, **kwargs)
         return TCPGitRequestHandler(self.handlers, *args, **kwargs)
 
 
     def __init__(self, backend, listen_addr, port=TCP_GIT_PORT, handlers=None) -> None:
     def __init__(self, backend, listen_addr, port=TCP_GIT_PORT, handlers=None) -> None:
+        """Initialize TCP git server.
+
+        Args:
+            backend: Backend instance
+            listen_addr: Address to listen on
+            port: Port to listen on (default: TCP_GIT_PORT)
+            handlers: Optional dictionary of custom handlers
+        """
         self.handlers = dict(DEFAULT_HANDLERS)
         self.handlers = dict(DEFAULT_HANDLERS)
         if handlers is not None:
         if handlers is not None:
             self.handlers.update(handlers)
             self.handlers.update(handlers)
@@ -1319,10 +1485,25 @@ class TCPGitServer(socketserver.TCPServer):
         socketserver.TCPServer.__init__(self, (listen_addr, port), self._make_handler)
         socketserver.TCPServer.__init__(self, (listen_addr, port), self._make_handler)
 
 
     def verify_request(self, request, client_address) -> bool:
     def verify_request(self, request, client_address) -> bool:
+        """Verify incoming request.
+
+        Args:
+            request: Request socket
+            client_address: Client address tuple
+
+        Returns:
+            True to accept request
+        """
         logger.info("Handling request from %s", client_address)
         logger.info("Handling request from %s", client_address)
         return True
         return True
 
 
     def handle_error(self, request, client_address) -> None:
     def handle_error(self, request, client_address) -> None:
+        """Handle request processing errors.
+
+        Args:
+            request: Request socket
+            client_address: Client address tuple
+        """
         logger.exception(
         logger.exception(
             "Exception happened during processing of request from %s",
             "Exception happened during processing of request from %s",
             client_address,
             client_address,

+ 1 - 2
dulwich/sparse_patterns.py

@@ -39,8 +39,7 @@ class BlobNotFoundError(Exception):
 
 
 
 
 def determine_included_paths(index: Index, lines: list[str], cone: bool) -> set[str]:
 def determine_included_paths(index: Index, lines: list[str], cone: bool) -> set[str]:
-    """Determine which paths in the index should be included based on either
-    a full-pattern match or a cone-mode approach.
+    """Determine which paths in the index should be included based on either a full-pattern match or a cone-mode approach.
 
 
     Args:
     Args:
       index: An Index object containing the repository's index.
       index: An Index object containing the repository's index.

+ 13 - 0
dulwich/stash.py

@@ -68,6 +68,12 @@ class Stash:
     """
     """
 
 
     def __init__(self, repo: "Repo", ref: Ref = DEFAULT_STASH_REF) -> None:
     def __init__(self, repo: "Repo", ref: Ref = DEFAULT_STASH_REF) -> None:
+        """Initialize Stash.
+
+        Args:
+          repo: Repository object
+          ref: Stash reference name
+        """
         self._ref = ref
         self._ref = ref
         self._repo = repo
         self._repo = repo
 
 
@@ -76,6 +82,11 @@ class Stash:
         return os.path.join(self._repo.commondir(), "logs", os.fsdecode(self._ref))
         return os.path.join(self._repo.commondir(), "logs", os.fsdecode(self._ref))
 
 
     def stashes(self) -> list["Entry"]:
     def stashes(self) -> list["Entry"]:
+        """Get list of stash entries.
+
+        Returns:
+          List of stash entries in chronological order
+        """
         try:
         try:
             with GitFile(self._reflog_path, "rb") as f:
             with GitFile(self._reflog_path, "rb") as f:
                 return list(reversed(list(read_reflog(f))))
                 return list(reversed(list(read_reflog(f))))
@@ -330,7 +341,9 @@ class Stash:
         return cid
         return cid
 
 
     def __getitem__(self, index: int) -> "Entry":
     def __getitem__(self, index: int) -> "Entry":
+        """Get stash entry by index."""
         return list(self.stashes())[index]
         return list(self.stashes())[index]
 
 
     def __len__(self) -> int:
     def __len__(self) -> int:
+        """Return number of stash entries."""
         return len(list(self.stashes()))
         return len(list(self.stashes()))

+ 41 - 3
dulwich/tests/test_object_store.py

@@ -50,6 +50,8 @@ testobject = make_object(Blob, data=b"yummy data")
 
 
 
 
 class ObjectStoreTests:
 class ObjectStoreTests:
+    """Base class for testing object store implementations."""
+
     store: "BaseObjectStore"
     store: "BaseObjectStore"
 
 
     assertEqual: Callable[[object, object], None]
     assertEqual: Callable[[object, object], None]
@@ -62,17 +64,20 @@ class ObjectStoreTests:
     assertFalse: Callable[[bool], None]
     assertFalse: Callable[[bool], None]
 
 
     def test_determine_wants_all(self) -> None:
     def test_determine_wants_all(self) -> None:
+        """Test determine_wants_all with valid ref."""
         self.assertEqual(
         self.assertEqual(
             [b"1" * 40],
             [b"1" * 40],
             self.store.determine_wants_all({b"refs/heads/foo": b"1" * 40}),
             self.store.determine_wants_all({b"refs/heads/foo": b"1" * 40}),
         )
         )
 
 
     def test_determine_wants_all_zero(self) -> None:
     def test_determine_wants_all_zero(self) -> None:
+        """Test determine_wants_all with zero ref."""
         self.assertEqual(
         self.assertEqual(
             [], self.store.determine_wants_all({b"refs/heads/foo": b"0" * 40})
             [], self.store.determine_wants_all({b"refs/heads/foo": b"0" * 40})
         )
         )
 
 
     def test_determine_wants_all_depth(self) -> None:
     def test_determine_wants_all_depth(self) -> None:
+        """Test determine_wants_all with depth parameter."""
         self.store.add_object(testobject)
         self.store.add_object(testobject)
         refs = {b"refs/heads/foo": testobject.id}
         refs = {b"refs/heads/foo": testobject.id}
         with patch.object(self.store, "_get_depth", return_value=1) as m:
         with patch.object(self.store, "_get_depth", return_value=1) as m:
@@ -90,6 +95,7 @@ class ObjectStoreTests:
             )
             )
 
 
     def test_get_depth(self) -> None:
     def test_get_depth(self) -> None:
+        """Test getting object depth."""
         self.assertEqual(0, self.store._get_depth(testobject.id))
         self.assertEqual(0, self.store._get_depth(testobject.id))
 
 
         self.store.add_object(testobject)
         self.store.add_object(testobject)
@@ -108,26 +114,29 @@ class ObjectStoreTests:
         )
         )
 
 
     def test_iter(self) -> None:
     def test_iter(self) -> None:
+        """Test iterating over empty store."""
         self.assertEqual([], list(self.store))
         self.assertEqual([], list(self.store))
 
 
     def test_get_nonexistant(self) -> None:
     def test_get_nonexistant(self) -> None:
+        """Test getting non-existent object raises KeyError."""
         self.assertRaises(KeyError, lambda: self.store[b"a" * 40])
         self.assertRaises(KeyError, lambda: self.store[b"a" * 40])
 
 
     def test_contains_nonexistant(self) -> None:
     def test_contains_nonexistant(self) -> None:
+        """Test checking for non-existent object."""
         self.assertNotIn(b"a" * 40, self.store)
         self.assertNotIn(b"a" * 40, self.store)
 
 
     def test_add_objects_empty(self) -> None:
     def test_add_objects_empty(self) -> None:
+        """Test adding empty list of objects."""
         self.store.add_objects([])
         self.store.add_objects([])
 
 
     def test_add_commit(self) -> None:
     def test_add_commit(self) -> None:
+        """Test adding commit objects."""
         # TODO: Argh, no way to construct Git commit objects without
         # TODO: Argh, no way to construct Git commit objects without
         # access to a serialized form.
         # access to a serialized form.
         self.store.add_objects([])
         self.store.add_objects([])
 
 
     def test_store_resilience(self) -> None:
     def test_store_resilience(self) -> None:
-        """Test if updating an existing stored object doesn't erase the
-        object from the store.
-        """
+        """Test if updating an existing stored object doesn't erase the object from the store."""
         test_object = make_object(Blob, data=b"data")
         test_object = make_object(Blob, data=b"data")
 
 
         self.store.add_object(test_object)
         self.store.add_object(test_object)
@@ -139,6 +148,7 @@ class ObjectStoreTests:
         self.assertEqual(stored_test_object.id, test_object_id)
         self.assertEqual(stored_test_object.id, test_object_id)
 
 
     def test_add_object(self) -> None:
     def test_add_object(self) -> None:
+        """Test adding a single object to store."""
         self.store.add_object(testobject)
         self.store.add_object(testobject)
         self.assertEqual({testobject.id}, set(self.store))
         self.assertEqual({testobject.id}, set(self.store))
         self.assertIn(testobject.id, self.store)
         self.assertIn(testobject.id, self.store)
@@ -146,6 +156,7 @@ class ObjectStoreTests:
         self.assertEqual(r, testobject)
         self.assertEqual(r, testobject)
 
 
     def test_add_objects(self) -> None:
     def test_add_objects(self) -> None:
+        """Test adding multiple objects to store."""
         data = [(testobject, "mypath")]
         data = [(testobject, "mypath")]
         self.store.add_objects(data)
         self.store.add_objects(data)
         self.assertEqual({testobject.id}, set(self.store))
         self.assertEqual({testobject.id}, set(self.store))
@@ -154,6 +165,7 @@ class ObjectStoreTests:
         self.assertEqual(r, testobject)
         self.assertEqual(r, testobject)
 
 
     def test_tree_changes(self) -> None:
     def test_tree_changes(self) -> None:
+        """Test detecting changes between trees."""
         blob_a1 = make_object(Blob, data=b"a1")
         blob_a1 = make_object(Blob, data=b"a1")
         blob_a2 = make_object(Blob, data=b"a2")
         blob_a2 = make_object(Blob, data=b"a2")
         blob_b = make_object(Blob, data=b"b")
         blob_b = make_object(Blob, data=b"b")
@@ -179,6 +191,7 @@ class ObjectStoreTests:
         )
         )
 
 
     def test_iter_tree_contents(self) -> None:
     def test_iter_tree_contents(self) -> None:
+        """Test iterating over tree contents."""
         blob_a = make_object(Blob, data=b"a")
         blob_a = make_object(Blob, data=b"a")
         blob_b = make_object(Blob, data=b"b")
         blob_b = make_object(Blob, data=b"b")
         blob_c = make_object(Blob, data=b"c")
         blob_c = make_object(Blob, data=b"c")
@@ -200,6 +213,7 @@ class ObjectStoreTests:
         self.assertEqual([], list(iter_tree_contents(self.store, None)))
         self.assertEqual([], list(iter_tree_contents(self.store, None)))
 
 
     def test_iter_tree_contents_include_trees(self) -> None:
     def test_iter_tree_contents_include_trees(self) -> None:
+        """Test iterating tree contents including tree objects."""
         blob_a = make_object(Blob, data=b"a")
         blob_a = make_object(Blob, data=b"a")
         blob_b = make_object(Blob, data=b"b")
         blob_b = make_object(Blob, data=b"b")
         blob_c = make_object(Blob, data=b"c")
         blob_c = make_object(Blob, data=b"c")
@@ -230,11 +244,13 @@ class ObjectStoreTests:
         self.assertEqual(expected, list(actual))
         self.assertEqual(expected, list(actual))
 
 
     def make_tag(self, name, obj):
     def make_tag(self, name, obj):
+        """Helper to create and add a tag object."""
         tag = make_tag(obj, name=name)
         tag = make_tag(obj, name=name)
         self.store.add_object(tag)
         self.store.add_object(tag)
         return tag
         return tag
 
 
     def test_peel_sha(self) -> None:
     def test_peel_sha(self) -> None:
+        """Test peeling SHA to get underlying object."""
         self.store.add_object(testobject)
         self.store.add_object(testobject)
         tag1 = self.make_tag(b"1", testobject)
         tag1 = self.make_tag(b"1", testobject)
         tag2 = self.make_tag(b"2", testobject)
         tag2 = self.make_tag(b"2", testobject)
@@ -243,17 +259,20 @@ class ObjectStoreTests:
             self.assertEqual((obj, testobject), peel_sha(self.store, obj.id))
             self.assertEqual((obj, testobject), peel_sha(self.store, obj.id))
 
 
     def test_get_raw(self) -> None:
     def test_get_raw(self) -> None:
+        """Test getting raw object data."""
         self.store.add_object(testobject)
         self.store.add_object(testobject)
         self.assertEqual(
         self.assertEqual(
             (Blob.type_num, b"yummy data"), self.store.get_raw(testobject.id)
             (Blob.type_num, b"yummy data"), self.store.get_raw(testobject.id)
         )
         )
 
 
     def test_close(self) -> None:
     def test_close(self) -> None:
+        """Test closing the object store."""
         # For now, just check that close doesn't barf.
         # For now, just check that close doesn't barf.
         self.store.add_object(testobject)
         self.store.add_object(testobject)
         self.store.close()
         self.store.close()
 
 
     def test_iter_prefix(self) -> None:
     def test_iter_prefix(self) -> None:
+        """Test iterating objects by prefix."""
         self.store.add_object(testobject)
         self.store.add_object(testobject)
         self.assertEqual([testobject.id], list(self.store.iter_prefix(testobject.id)))
         self.assertEqual([testobject.id], list(self.store.iter_prefix(testobject.id)))
         self.assertEqual(
         self.assertEqual(
@@ -296,20 +315,26 @@ class ObjectStoreTests:
         self.assertEqual(blob1.id, objects[0].id)
         self.assertEqual(blob1.id, objects[0].id)
 
 
     def test_iter_prefix_not_found(self) -> None:
     def test_iter_prefix_not_found(self) -> None:
+        """Test iterating with prefix that doesn't match any objects."""
         self.assertEqual([], list(self.store.iter_prefix(b"1" * 40)))
         self.assertEqual([], list(self.store.iter_prefix(b"1" * 40)))
 
 
 
 
 class PackBasedObjectStoreTests(ObjectStoreTests):
 class PackBasedObjectStoreTests(ObjectStoreTests):
+    """Tests for pack-based object stores."""
+
     store: PackBasedObjectStore
     store: PackBasedObjectStore
 
 
     def tearDown(self) -> None:
     def tearDown(self) -> None:
+        """Clean up by closing all packs."""
         for pack in self.store.packs:
         for pack in self.store.packs:
             pack.close()
             pack.close()
 
 
     def test_empty_packs(self) -> None:
     def test_empty_packs(self) -> None:
+        """Test that new store has no packs."""
         self.assertEqual([], list(self.store.packs))
         self.assertEqual([], list(self.store.packs))
 
 
     def test_pack_loose_objects(self) -> None:
     def test_pack_loose_objects(self) -> None:
+        """Test packing loose objects into packs."""
         b1 = make_object(Blob, data=b"yummy data")
         b1 = make_object(Blob, data=b"yummy data")
         self.store.add_object(b1)
         self.store.add_object(b1)
         b2 = make_object(Blob, data=b"more yummy data")
         b2 = make_object(Blob, data=b"more yummy data")
@@ -324,6 +349,7 @@ class PackBasedObjectStoreTests(ObjectStoreTests):
         self.assertEqual(0, self.store.pack_loose_objects())
         self.assertEqual(0, self.store.pack_loose_objects())
 
 
     def test_repack(self) -> None:
     def test_repack(self) -> None:
+        """Test repacking multiple packs into one."""
         b1 = make_object(Blob, data=b"yummy data")
         b1 = make_object(Blob, data=b"yummy data")
         self.store.add_object(b1)
         self.store.add_object(b1)
         b2 = make_object(Blob, data=b"more yummy data")
         b2 = make_object(Blob, data=b"more yummy data")
@@ -341,6 +367,7 @@ class PackBasedObjectStoreTests(ObjectStoreTests):
         self.assertEqual(0, self.store.pack_loose_objects())
         self.assertEqual(0, self.store.pack_loose_objects())
 
 
     def test_repack_existing(self) -> None:
     def test_repack_existing(self) -> None:
+        """Test repacking with existing objects."""
         b1 = make_object(Blob, data=b"yummy data")
         b1 = make_object(Blob, data=b"yummy data")
         self.store.add_object(b1)
         self.store.add_object(b1)
         b2 = make_object(Blob, data=b"more yummy data")
         b2 = make_object(Blob, data=b"more yummy data")
@@ -401,16 +428,21 @@ class PackBasedObjectStoreTests(ObjectStoreTests):
 
 
 
 
 class FindShallowTests(TestCase):
 class FindShallowTests(TestCase):
+    """Tests for finding shallow commits."""
+
     def setUp(self):
     def setUp(self):
+        """Set up test fixture."""
         super().setUp()
         super().setUp()
         self._store = MemoryObjectStore()
         self._store = MemoryObjectStore()
 
 
     def make_commit(self, **attrs):
     def make_commit(self, **attrs):
+        """Helper to create and store a commit."""
         commit = make_commit(**attrs)
         commit = make_commit(**attrs)
         self._store.add_object(commit)
         self._store.add_object(commit)
         return commit
         return commit
 
 
     def make_linear_commits(self, n, message=b""):
     def make_linear_commits(self, n, message=b""):
+        """Create a linear chain of commits."""
         commits = []
         commits = []
         parents = []
         parents = []
         for _ in range(n):
         for _ in range(n):
@@ -419,9 +451,11 @@ class FindShallowTests(TestCase):
         return commits
         return commits
 
 
     def assertSameElements(self, expected, actual):
     def assertSameElements(self, expected, actual):
+        """Assert that two sequences contain the same elements."""
         self.assertEqual(set(expected), set(actual))
         self.assertEqual(set(expected), set(actual))
 
 
     def test_linear(self):
     def test_linear(self):
+        """Test finding shallow commits in a linear history."""
         c1, c2, c3 = self.make_linear_commits(3)
         c1, c2, c3 = self.make_linear_commits(3)
 
 
         self.assertEqual((set([c3.id]), set([])), find_shallow(self._store, [c3.id], 1))
         self.assertEqual((set([c3.id]), set([])), find_shallow(self._store, [c3.id], 1))
@@ -439,6 +473,7 @@ class FindShallowTests(TestCase):
         )
         )
 
 
     def test_multiple_independent(self):
     def test_multiple_independent(self):
+        """Test finding shallow commits with multiple independent branches."""
         a = self.make_linear_commits(2, message=b"a")
         a = self.make_linear_commits(2, message=b"a")
         b = self.make_linear_commits(2, message=b"b")
         b = self.make_linear_commits(2, message=b"b")
         c = self.make_linear_commits(2, message=b"c")
         c = self.make_linear_commits(2, message=b"c")
@@ -450,6 +485,7 @@ class FindShallowTests(TestCase):
         )
         )
 
 
     def test_multiple_overlapping(self):
     def test_multiple_overlapping(self):
+        """Test finding shallow commits with overlapping branches."""
         # Create the following commit tree:
         # Create the following commit tree:
         # 1--2
         # 1--2
         #  \
         #  \
@@ -465,6 +501,7 @@ class FindShallowTests(TestCase):
         )
         )
 
 
     def test_merge(self):
     def test_merge(self):
+        """Test finding shallow commits with merge commits."""
         c1 = self.make_commit()
         c1 = self.make_commit()
         c2 = self.make_commit()
         c2 = self.make_commit()
         c3 = self.make_commit(parents=[c1.id, c2.id])
         c3 = self.make_commit(parents=[c1.id, c2.id])
@@ -475,6 +512,7 @@ class FindShallowTests(TestCase):
         )
         )
 
 
     def test_tag(self):
     def test_tag(self):
+        """Test finding shallow commits with tags."""
         c1, c2 = self.make_linear_commits(2)
         c1, c2 = self.make_linear_commits(2)
         tag = make_tag(c2, name=b"tag")
         tag = make_tag(c2, name=b"tag")
         self._store.add_object(tag)
         self._store.add_object(tag)

+ 1 - 0
dulwich/tests/utils.py

@@ -87,6 +87,7 @@ def make_object(cls, **attrs):
     __slots__.
     __slots__.
 
 
     Args:
     Args:
+      cls: The class to create an instance of
       attrs: dict of attributes to set on the new object.
       attrs: dict of attributes to set on the new object.
     Returns: A newly initialized object of type cls.
     Returns: A newly initialized object of type cls.
     """
     """

+ 8 - 0
dulwich/walk.py

@@ -53,6 +53,12 @@ class WalkEntry:
     """Object encapsulating a single result from a walk."""
     """Object encapsulating a single result from a walk."""
 
 
     def __init__(self, walker: "Walker", commit: Commit) -> None:
     def __init__(self, walker: "Walker", commit: Commit) -> None:
+        """Initialize WalkEntry.
+
+        Args:
+          walker: Walker instance that created this entry
+          commit: Commit object for this entry
+        """
         self.commit = commit
         self.commit = commit
         self._store = walker.store
         self._store = walker.store
         self._get_parents = walker.get_parents
         self._get_parents = walker.get_parents
@@ -141,6 +147,7 @@ class WalkEntry:
         return self._changes[path_prefix]
         return self._changes[path_prefix]
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of WalkEntry."""
         return f"<WalkEntry commit={self.commit.id.decode('ascii')}, changes={self.changes()!r}>"
         return f"<WalkEntry commit={self.commit.id.decode('ascii')}, changes={self.changes()!r}>"
 
 
 
 
@@ -435,6 +442,7 @@ class Walker:
         return results
         return results
 
 
     def __iter__(self) -> Iterator[WalkEntry]:
     def __iter__(self) -> Iterator[WalkEntry]:
+        """Iterate over walk entries."""
         return iter(self._reorder(iter(self._next, None)))
         return iter(self._reorder(iter(self._next, None)))
 
 
 
 

+ 163 - 9
dulwich/web.py

@@ -67,6 +67,14 @@ NO_CACHE_HEADERS = [
 
 
 
 
 def cache_forever_headers(now: Optional[float] = None) -> list[tuple[str, str]]:
 def cache_forever_headers(now: Optional[float] = None) -> list[tuple[str, str]]:
+    """Generate headers for caching forever.
+
+    Args:
+      now: Timestamp to use as base (defaults to current time)
+
+    Returns:
+      List of (header_name, header_value) tuples for caching forever
+    """
     if now is None:
     if now is None:
         now = time.time()
         now = time.time()
     return [
     return [
@@ -77,6 +85,14 @@ def cache_forever_headers(now: Optional[float] = None) -> list[tuple[str, str]]:
 
 
 
 
 def date_time_string(timestamp: Optional[float] = None) -> str:
 def date_time_string(timestamp: Optional[float] = None) -> str:
+    """Convert a timestamp to an HTTP date string.
+
+    Args:
+      timestamp: Unix timestamp to convert (defaults to current time)
+
+    Returns:
+      HTTP date string in RFC 1123 format
+    """
     # From BaseHTTPRequestHandler.date_time_string in BaseHTTPServer.py in the
     # From BaseHTTPRequestHandler.date_time_string in BaseHTTPServer.py in the
     # Python 2.6.5 standard library, following modifications:
     # Python 2.6.5 standard library, following modifications:
     #  - Made a global rather than an instance method.
     #  - Made a global rather than an instance method.
@@ -164,6 +180,16 @@ def _url_to_path(url: str) -> str:
 def get_text_file(
 def get_text_file(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> Iterator[bytes]:
 ) -> Iterator[bytes]:
+    """Send a plain text file from the repository.
+
+    Args:
+      req: The HTTP request object
+      backend: The git backend
+      mat: The regex match for the requested path
+
+    Returns:
+      Iterator yielding file contents as bytes
+    """
     req.nocache()
     req.nocache()
     path = _url_to_path(mat.group())
     path = _url_to_path(mat.group())
     logger.info("Sending plain text file %s", path)
     logger.info("Sending plain text file %s", path)
@@ -173,6 +199,16 @@ def get_text_file(
 def get_loose_object(
 def get_loose_object(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> Iterator[bytes]:
 ) -> Iterator[bytes]:
+    """Send a loose git object.
+
+    Args:
+      req: The HTTP request object
+      backend: The git backend
+      mat: The regex match containing object path segments
+
+    Returns:
+      Iterator yielding object contents as bytes
+    """
     sha = (mat.group(1) + mat.group(2)).encode("ascii")
     sha = (mat.group(1) + mat.group(2)).encode("ascii")
     logger.info("Sending loose object %s", sha)
     logger.info("Sending loose object %s", sha)
     object_store = get_repo(backend, mat).object_store
     object_store = get_repo(backend, mat).object_store
@@ -192,6 +228,16 @@ def get_loose_object(
 def get_pack_file(
 def get_pack_file(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> Iterator[bytes]:
 ) -> Iterator[bytes]:
+    """Send a git pack file.
+
+    Args:
+      req: The HTTP request object
+      backend: The git backend
+      mat: The regex match for the requested pack file
+
+    Returns:
+      Iterator yielding pack file contents as bytes
+    """
     req.cache_forever()
     req.cache_forever()
     path = _url_to_path(mat.group())
     path = _url_to_path(mat.group())
     logger.info("Sending pack file %s", path)
     logger.info("Sending pack file %s", path)
@@ -205,6 +251,16 @@ def get_pack_file(
 def get_idx_file(
 def get_idx_file(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> Iterator[bytes]:
 ) -> Iterator[bytes]:
+    """Send a git pack index file.
+
+    Args:
+      req: The HTTP request object
+      backend: The git backend
+      mat: The regex match for the requested index file
+
+    Returns:
+      Iterator yielding index file contents as bytes
+    """
     req.cache_forever()
     req.cache_forever()
     path = _url_to_path(mat.group())
     path = _url_to_path(mat.group())
     logger.info("Sending pack file %s", path)
     logger.info("Sending pack file %s", path)
@@ -218,6 +274,16 @@ def get_idx_file(
 def get_info_refs(
 def get_info_refs(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> Iterator[bytes]:
 ) -> Iterator[bytes]:
+    """Send git info/refs for discovery.
+
+    Args:
+      req: The HTTP request object
+      backend: The git backend
+      mat: The regex match for the info/refs request
+
+    Returns:
+      Iterator yielding refs advertisement or info/refs contents
+    """
     params = parse_qs(req.environ["QUERY_STRING"])
     params = parse_qs(req.environ["QUERY_STRING"])
     service = params.get("service", [None])[0]
     service = params.get("service", [None])[0]
     try:
     try:
@@ -255,6 +321,16 @@ def get_info_refs(
 def get_info_packs(
 def get_info_packs(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> Iterator[bytes]:
 ) -> Iterator[bytes]:
+    """Send git info/packs file listing available packs.
+
+    Args:
+      req: The HTTP request object
+      backend: The git backend
+      mat: The regex match for the info/packs request
+
+    Returns:
+      Iterator yielding pack listing as bytes
+    """
     req.nocache()
     req.nocache()
     req.respond(HTTP_OK, "text/plain")
     req.respond(HTTP_OK, "text/plain")
     logger.info("Emulating dumb info/packs")
     logger.info("Emulating dumb info/packs")
@@ -275,10 +351,23 @@ class ChunkReader:
     """Reader for chunked transfer encoding streams."""
     """Reader for chunked transfer encoding streams."""
 
 
     def __init__(self, f: BinaryIO) -> None:
     def __init__(self, f: BinaryIO) -> None:
+        """Initialize ChunkReader.
+
+        Args:
+            f: Binary file-like object to read from
+        """
         self._iter = _chunk_iter(f)
         self._iter = _chunk_iter(f)
         self._buffer: list[bytes] = []
         self._buffer: list[bytes] = []
 
 
     def read(self, n: int) -> bytes:
     def read(self, n: int) -> bytes:
+        """Read n bytes from the chunked stream.
+
+        Args:
+          n: Number of bytes to read
+
+        Returns:
+          Up to n bytes of data
+        """
         while sum(map(len, self._buffer)) < n:
         while sum(map(len, self._buffer)) < n:
             try:
             try:
                 self._buffer.append(next(self._iter))
                 self._buffer.append(next(self._iter))
@@ -303,6 +392,14 @@ class _LengthLimitedFile:
         self._bytes_avail = max_bytes
         self._bytes_avail = max_bytes
 
 
     def read(self, size: int = -1) -> bytes:
     def read(self, size: int = -1) -> bytes:
+        """Read up to size bytes from the limited input.
+
+        Args:
+          size: Maximum number of bytes to read, or -1 for all available
+
+        Returns:
+          Up to size bytes of data
+        """
         if self._bytes_avail <= 0:
         if self._bytes_avail <= 0:
             return b""
             return b""
         if size == -1 or size > self._bytes_avail:
         if size == -1 or size > self._bytes_avail:
@@ -316,6 +413,16 @@ class _LengthLimitedFile:
 def handle_service_request(
 def handle_service_request(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> Iterator[bytes]:
 ) -> Iterator[bytes]:
+    """Handle a git service request (upload-pack or receive-pack).
+
+    Args:
+      req: The HTTP request object
+      backend: The git backend
+      mat: The regex match for the service request
+
+    Returns:
+      Iterator yielding service response as bytes
+    """
     service = mat.group().lstrip("/")
     service = mat.group().lstrip("/")
     logger.info("Handling service request for %s", service)
     logger.info("Handling service request for %s", service)
     handler_cls = req.handlers.get(service.encode("ascii"), None)
     handler_cls = req.handlers.get(service.encode("ascii"), None)
@@ -350,6 +457,14 @@ class HTTPGitRequest:
     def __init__(
     def __init__(
         self, environ, start_response, dumb: bool = False, handlers=None
         self, environ, start_response, dumb: bool = False, handlers=None
     ) -> None:
     ) -> None:
+        """Initialize HTTPGitRequest.
+
+        Args:
+            environ: WSGI environment dictionary
+            start_response: WSGI start_response callable
+            dumb: Whether to use dumb HTTP protocol
+            handlers: Optional handler overrides
+        """
         self.environ = environ
         self.environ = environ
         self.dumb = dumb
         self.dumb = dumb
         self.handlers = handlers
         self.handlers = handlers
@@ -443,6 +558,14 @@ class HTTPGitApplication:
     def __init__(
     def __init__(
         self, backend, dumb: bool = False, handlers=None, fallback_app=None
         self, backend, dumb: bool = False, handlers=None, fallback_app=None
     ) -> None:
     ) -> None:
+        """Initialize HTTPGitApplication.
+
+        Args:
+            backend: Backend object for git operations
+            dumb: Whether to use dumb HTTP protocol
+            handlers: Optional handler overrides
+            fallback_app: Optional fallback WSGI application
+        """
         self.backend = backend
         self.backend = backend
         self.dumb = dumb
         self.dumb = dumb
         self.handlers = dict(DEFAULT_HANDLERS)
         self.handlers = dict(DEFAULT_HANDLERS)
@@ -451,6 +574,7 @@ class HTTPGitApplication:
             self.handlers.update(handlers)
             self.handlers.update(handlers)
 
 
     def __call__(self, environ, start_response):
     def __call__(self, environ, start_response):
+        """Handle WSGI request."""
         path = environ["PATH_INFO"]
         path = environ["PATH_INFO"]
         method = environ["REQUEST_METHOD"]
         method = environ["REQUEST_METHOD"]
         req = HTTPGitRequest(
         req = HTTPGitRequest(
@@ -476,14 +600,14 @@ class HTTPGitApplication:
 
 
 
 
 class GunzipFilter:
 class GunzipFilter:
-    """WSGI middleware that unzips gzip-encoded requests before
-    passing on to the underlying application.
-    """
+    """WSGI middleware that unzips gzip-encoded requests before passing on to the underlying application."""
 
 
     def __init__(self, application) -> None:
     def __init__(self, application) -> None:
+        """Initialize GunzipFilter."""
         self.app = application
         self.app = application
 
 
     def __call__(self, environ, start_response):
     def __call__(self, environ, start_response):
+        """Handle WSGI request."""
         import gzip
         import gzip
 
 
         if environ.get("HTTP_CONTENT_ENCODING", "") == "gzip":
         if environ.get("HTTP_CONTENT_ENCODING", "") == "gzip":
@@ -498,14 +622,14 @@ class GunzipFilter:
 
 
 
 
 class LimitedInputFilter:
 class LimitedInputFilter:
-    """WSGI middleware that limits the input length of a request to that
-    specified in Content-Length.
-    """
+    """WSGI middleware that limits the input length of a request to that specified in Content-Length."""
 
 
     def __init__(self, application) -> None:
     def __init__(self, application) -> None:
+        """Initialize LimitedInputFilter."""
         self.app = application
         self.app = application
 
 
     def __call__(self, environ, start_response):
     def __call__(self, environ, start_response):
+        """Handle WSGI request."""
         # This is not necessary if this app is run from a conforming WSGI
         # This is not necessary if this app is run from a conforming WSGI
         # server. Unfortunately, there's no way to tell that at this point.
         # server. Unfortunately, there's no way to tell that at this point.
         # TODO: git may used HTTP/1.1 chunked encoding instead of specifying
         # TODO: git may used HTTP/1.1 chunked encoding instead of specifying
@@ -519,9 +643,7 @@ class LimitedInputFilter:
 
 
 
 
 def make_wsgi_chain(*args, **kwargs):
 def make_wsgi_chain(*args, **kwargs):
-    """Factory function to create an instance of HTTPGitApplication,
-    correctly wrapped with needed middleware.
-    """
+    """Factory function to create an instance of HTTPGitApplication, correctly wrapped with needed middleware."""
     app = HTTPGitApplication(*args, **kwargs)
     app = HTTPGitApplication(*args, **kwargs)
     wrapped_app = LimitedInputFilter(GunzipFilter(app))
     wrapped_app = LimitedInputFilter(GunzipFilter(app))
     return wrapped_app
     return wrapped_app
@@ -531,15 +653,31 @@ class ServerHandlerLogger(ServerHandler):
     """ServerHandler that uses dulwich's logger for logging exceptions."""
     """ServerHandler that uses dulwich's logger for logging exceptions."""
 
 
     def log_exception(self, exc_info) -> None:
     def log_exception(self, exc_info) -> None:
+        """Log an exception using dulwich's logger.
+
+        Args:
+          exc_info: Exception information tuple
+        """
         logger.exception(
         logger.exception(
             "Exception happened during processing of request",
             "Exception happened during processing of request",
             exc_info=exc_info,
             exc_info=exc_info,
         )
         )
 
 
     def log_message(self, format, *args) -> None:
     def log_message(self, format, *args) -> None:
+        """Log a message using dulwich's logger.
+
+        Args:
+          format: Format string for the message
+          *args: Arguments for the format string
+        """
         logger.info(format, *args)
         logger.info(format, *args)
 
 
     def log_error(self, *args) -> None:
     def log_error(self, *args) -> None:
+        """Log an error using dulwich's logger.
+
+        Args:
+          *args: Error message components
+        """
         logger.error(*args)
         logger.error(*args)
 
 
 
 
@@ -547,15 +685,31 @@ class WSGIRequestHandlerLogger(WSGIRequestHandler):
     """WSGIRequestHandler that uses dulwich's logger for logging exceptions."""
     """WSGIRequestHandler that uses dulwich's logger for logging exceptions."""
 
 
     def log_exception(self, exc_info) -> None:
     def log_exception(self, exc_info) -> None:
+        """Log an exception using dulwich's logger.
+
+        Args:
+          exc_info: Exception information tuple
+        """
         logger.exception(
         logger.exception(
             "Exception happened during processing of request",
             "Exception happened during processing of request",
             exc_info=exc_info,
             exc_info=exc_info,
         )
         )
 
 
     def log_message(self, format, *args) -> None:
     def log_message(self, format, *args) -> None:
+        """Log a message using dulwich's logger.
+
+        Args:
+          format: Format string for the message
+          *args: Arguments for the format string
+        """
         logger.info(format, *args)
         logger.info(format, *args)
 
 
     def log_error(self, *args) -> None:
     def log_error(self, *args) -> None:
+        """Log an error using dulwich's logger.
+
+        Args:
+          *args: Error message components
+        """
         logger.error(*args)
         logger.error(*args)
 
 
     def handle(self) -> None:
     def handle(self) -> None:

+ 16 - 1
dulwich/worktree.py

@@ -72,6 +72,18 @@ class WorkTreeInfo:
         prunable: bool = False,
         prunable: bool = False,
         lock_reason: str | None = None,
         lock_reason: str | None = None,
     ):
     ):
+        """Initialize WorkTreeInfo.
+
+        Args:
+          path: Path to the worktree
+          head: Current HEAD commit SHA
+          branch: Current branch (if not detached)
+          bare: Whether this is a bare repository
+          detached: Whether HEAD is detached
+          locked: Whether the worktree is locked
+          prunable: Whether the worktree can be pruned
+          lock_reason: Reason for locking (if locked)
+        """
         self.path = path
         self.path = path
         self.head = head
         self.head = head
         self.branch = branch
         self.branch = branch
@@ -82,9 +94,11 @@ class WorkTreeInfo:
         self.lock_reason = lock_reason
         self.lock_reason = lock_reason
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
+        """Return string representation of WorkTreeInfo."""
         return f"WorkTreeInfo(path={self.path!r}, branch={self.branch!r}, detached={self.detached})"
         return f"WorkTreeInfo(path={self.path!r}, branch={self.branch!r}, detached={self.detached})"
 
 
     def __eq__(self, other: object) -> bool:
     def __eq__(self, other: object) -> bool:
+        """Check equality with another WorkTreeInfo."""
         if not isinstance(other, WorkTreeInfo):
         if not isinstance(other, WorkTreeInfo):
             return NotImplemented
             return NotImplemented
         return (
         return (
@@ -311,7 +325,8 @@ class WorkTree:
         index.write()
         index.write()
 
 
     def unstage(self, fs_paths: list[str]) -> None:
     def unstage(self, fs_paths: list[str]) -> None:
-        """Unstage specific file in the index
+        """Unstage specific file in the index.
+
         Args:
         Args:
           fs_paths: a list of files to unstage,
           fs_paths: a list of files to unstage,
             relative to the repository path.
             relative to the repository path.

+ 8 - 10
pyproject.toml

@@ -98,22 +98,20 @@ ignore = [
     "ANN204",
     "ANN204",
     "ANN205",
     "ANN205",
     "ANN206",
     "ANN206",
-    "D100",
-    "D101",
-    "D102",
-    "D103",
-    "D104",
-    "D105",
-    "D107",
-    "D204",
-    "D205",
-    "D417",
     "E501",  # line too long
     "E501",  # line too long
 ]
 ]
 
 
 [tool.ruff.lint.pydocstyle]
 [tool.ruff.lint.pydocstyle]
 convention = "google"
 convention = "google"
 
 
+[tool.ruff.lint.per-file-ignores]
+"tests/**/*.py" = ["D"]  # Don't require docstrings in tests
+"fuzzing/**/*.py" = ["D"]  # Don't require docstrings in fuzzing
+"examples/**/*.py" = ["D"]  # Don't require docstrings in examples
+"devscripts/**/*.py" = ["D"]  # Don't require docstrings in devscripts
+"docs/conf.py" = ["D"]  # Don't require docstrings in docs config
+"setup.py" = ["D"]  # Don't require docstrings in setup.py
+
 [tool.cibuildwheel]
 [tool.cibuildwheel]
 skip = "cp314-*"
 skip = "cp314-*"
 environment = {PATH="$HOME/.cargo/bin:$PATH"}
 environment = {PATH="$HOME/.cargo/bin:$PATH"}

Некоторые файлы не были показаны из-за большого количества измененных файлов