Explorar o código

Add docstrings for everything (#1754)

Jelmer Vernooij hai 5 meses
pai
achega
778a183937
Modificáronse 53 ficheiros con 3303 adicións e 82 borrados
  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,
         remove_in: Optional[Union[str, tuple[int, ...]]] = None,
     ) -> 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:
             import functools
             import warnings

+ 15 - 3
dulwich/archive.py

@@ -51,10 +51,23 @@ class ChunkedBytesIO:
     """
 
     def __init__(self, contents: list[bytes]) -> None:
+        """Initialize ChunkedBytesIO.
+
+        Args:
+            contents: List of byte chunks
+        """
         self.contents = contents
         self.pos = (0, 0)
 
     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:
             remaining = None
         else:
@@ -98,6 +111,7 @@ def tar_stream(
       tree: Tree object for the tree root
       mtime: UNIX timestamp that is assigned as the modification time for
         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
     Returns:
       Bytestrings
@@ -150,9 +164,7 @@ def tar_stream(
 def _walk_tree(
     store: "BaseObjectStore", tree: "Tree", root: bytes = b""
 ) -> 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():
         entry_abspath = posixpath.join(root, entry.path)
         if stat.S_ISDIR(entry.mode):

+ 5 - 0
dulwich/attrs.py

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

+ 5 - 0
dulwich/bisect.py

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

+ 10 - 0
dulwich/bundle.py

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

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 422 - 0
dulwich/cli.py


+ 139 - 2
dulwich/client.py

@@ -150,6 +150,11 @@ class InvalidWants(Exception):
     """Invalid wants."""
 
     def __init__(self, wants) -> None:
+        """Initialize InvalidWants exception.
+
+        Args:
+            wants: List of invalid wants
+        """
         Exception.__init__(
             self, f"requested wants not in server provided refs: {wants!r}"
         )
@@ -159,6 +164,12 @@ class HTTPUnauthorized(Exception):
     """Raised when authentication fails."""
 
     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")
         self.www_authenticate = www_authenticate
         self.url = url
@@ -168,6 +179,12 @@ class HTTPProxyUnauthorized(Exception):
     """Raised when proxy authentication fails."""
 
     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")
         self.proxy_authenticate = proxy_authenticate
         self.url = url
@@ -211,6 +228,7 @@ class ReportStatusParser:
     """Handle status as reported by servers with 'report-status' capability."""
 
     def __init__(self) -> None:
+        """Initialize ReportStatusParser."""
         self._done = False
         self._pack_status = None
         self._ref_statuses: list[bytes] = []
@@ -409,6 +427,15 @@ class FetchPackResult(_DeprecatedDictProxy):
     def __init__(
         self, refs, symrefs, agent, new_shallow=None, new_unshallow=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.symrefs = symrefs
         self.agent = agent
@@ -440,6 +467,12 @@ class LsRemoteResult(_DeprecatedDictProxy):
     """
 
     def __init__(self, refs, symrefs) -> None:
+        """Initialize LsRemoteResult.
+
+        Args:
+            refs: Dictionary with all remote refs
+            symrefs: Dictionary with remote symrefs
+        """
         self.refs = refs
         self.symrefs = symrefs
 
@@ -476,6 +509,13 @@ class SendPackResult(_DeprecatedDictProxy):
     """
 
     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.agent = agent
         self.ref_status = ref_status
@@ -791,8 +831,10 @@ class GitClient:
           thin_packs: Whether or not thin packs should be retrieved
           report_activity: Optional callback for reporting transport
             activity.
+          quiet: Whether to suppress output
           include_tags: send annotated tags when sending the objects they point
             to
+          **kwargs: Additional keyword arguments
         """
         self._report_activity = report_activity
         self._report_status_parser: Optional[ReportStatusParser] = None
@@ -826,6 +868,7 @@ class GitClient:
 
         Args:
           parsedurl: Result of urlparse()
+          **kwargs: Additional keyword arguments passed to the client constructor
 
         Returns:
           A `GitClient` object
@@ -1233,6 +1276,12 @@ class TraditionalGitClient(GitClient):
     DEFAULT_ENCODING = "utf-8"
 
     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
         super().__init__(**kwargs)
 
@@ -1559,6 +1608,18 @@ class TraditionalGitClient(GitClient):
         subdirs=None,
         prefix=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)
         with proto:
             if format is not None:
@@ -1627,13 +1688,13 @@ class TCPGitClient(TraditionalGitClient):
         return cls(parsedurl.hostname, port=parsedurl.port, **kwargs)
 
     def get_url(self, path):
-        """Get the URL for a TCP git connection.
+        r"""Get the URL for a TCP git connection.
 
         Args:
           path: Repository path
 
         Returns:
-          git:// URL for the path
+          ``git://`` URL for the path
         """
         netloc = self._host
         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
           report_activity: Optional callback for reporting transport
             activity.
+          config: Optional configuration object
         """
         self._report_activity = report_activity
         # Ignore the thin_packs argument
@@ -1898,6 +1960,8 @@ class LocalGitClient(GitClient):
             Receive dict with existing remote refs, returns dict with
             changed refs (name -> sha, where sha=ZERO_SHA for deletions)
             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
 
         Returns:
@@ -1979,6 +2043,8 @@ class LocalGitClient(GitClient):
           filter_spec: A git-rev-list-style object filter spec, as bytestring.
             Only used if the server supports the Git protocol-v2 'filter'
             feature, and ignored otherwise.
+          protocol_version: Optional Git protocol version
+          **kwargs: Additional keyword arguments
 
         Returns:
           FetchPackResult object
@@ -2022,6 +2088,7 @@ class LocalGitClient(GitClient):
           filter_spec: A git-rev-list-style object filter spec, as bytestring.
             Only used if the server supports the Git protocol-v2 'filter'
             feature, and ignored otherwise.
+          protocol_version: Optional Git protocol version
 
         Returns:
           FetchPackResult object
@@ -2084,6 +2151,7 @@ class BundleClient(GitClient):
           thin_packs: Whether or not thin packs should be retrieved
           report_activity: Optional callback for reporting transport
             activity.
+          config: Optional configuration object
         """
         self._report_activity = report_activity
 
@@ -2383,6 +2451,11 @@ class StrangeHostname(Exception):
     """Refusing to connect to strange SSH hostname."""
 
     def __init__(self, hostname) -> None:
+        """Initialize StrangeHostname exception.
+
+        Args:
+            hostname: The strange hostname that was rejected
+        """
         super().__init__(hostname)
 
 
@@ -2400,6 +2473,21 @@ class SubprocessSSHVendor(SSHVendor):
         ssh_command=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:
             raise NotImplementedError(
                 "Setting password not supported by SubprocessSSHVendor."
@@ -2453,6 +2541,21 @@ class PLinkSSHVendor(SSHVendor):
         ssh_command=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:
             import shlex
 
@@ -2506,6 +2609,7 @@ class PLinkSSHVendor(SSHVendor):
 
 
 def ParamikoSSHVendor(**kwargs):
+    """Create a ParamikoSSHVendor (deprecated)."""
     import warnings
 
     warnings.warn(
@@ -2522,6 +2626,8 @@ get_ssh_vendor: Callable[[], SSHVendor] = SubprocessSSHVendor
 
 
 class SSHGitClient(TraditionalGitClient):
+    """Git client that connects over SSH."""
+
     def __init__(
         self,
         host,
@@ -2534,6 +2640,19 @@ class SSHGitClient(TraditionalGitClient):
         ssh_command=None,
         **kwargs,
     ) -> 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.port = port
         self.username = username
@@ -2566,6 +2685,7 @@ class SSHGitClient(TraditionalGitClient):
             self.ssh_vendor = get_ssh_vendor()
 
     def get_url(self, path):
+        """Get the SSH URL for a path."""
         netloc = self.host
         if self.port is not None:
             netloc += f":{self.port}"
@@ -2577,6 +2697,7 @@ class SSHGitClient(TraditionalGitClient):
 
     @classmethod
     def from_parsedurl(cls, parsedurl, **kwargs):
+        """Create an SSHGitClient from a parsed URL."""
         return cls(
             host=parsedurl.hostname,
             port=parsedurl.port,
@@ -2636,6 +2757,7 @@ class SSHGitClient(TraditionalGitClient):
 
 
 def default_user_agent_string():
+    """Return the default user agent string for Dulwich."""
     # Start user agent with "git/", because GitHub requires this. :-( See
     # https://github.com/jelmer/dulwich/issues/562 for details.
     return "git/dulwich/{}".format(".".join([str(x) for x in dulwich.__version__]))
@@ -2655,6 +2777,9 @@ def default_urllib3_manager(
 
     Args:
       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
       override_kwargs: Additional arguments for `urllib3.ProxyManager`
 
@@ -2756,6 +2881,7 @@ def default_urllib3_manager(
 
 
 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
     if base_url:  # only check if base_url is provided
         no_proxy_str = os.environ.get("no_proxy")
@@ -2819,6 +2945,7 @@ class AbstractHttpGitClient(GitClient):
     """
 
     def __init__(self, base_url, dumb=False, **kwargs) -> None:
+        """Initialize AbstractHttpGitClient."""
         self._base_url = base_url.rstrip("/") + "/"
         self.dumb = dumb
         GitClient.__init__(self, **kwargs)
@@ -2830,6 +2957,7 @@ class AbstractHttpGitClient(GitClient):
           url: Request URL.
           headers: Optional custom headers to override defaults.
           data: Request data.
+          raise_for_status: Whether to raise an exception for HTTP errors.
 
         Returns:
           Tuple (response, read), where response is an urllib3
@@ -3230,6 +3358,7 @@ class AbstractHttpGitClient(GitClient):
         return LsRemoteResult(refs, symrefs)
 
     def get_url(self, path):
+        """Get the HTTP URL for a path."""
         return self._get_url(path).rstrip("/")
 
     def _get_url(self, path):
@@ -3237,6 +3366,7 @@ class AbstractHttpGitClient(GitClient):
 
     @classmethod
     def from_parsedurl(cls, parsedurl, **kwargs):
+        """Create an AbstractHttpGitClient from a parsed URL."""
         password = parsedurl.password
         if password is not None:
             kwargs["password"] = urlunquote(password)
@@ -3246,6 +3376,7 @@ class AbstractHttpGitClient(GitClient):
         return cls(urlunparse(parsedurl), **kwargs)
 
     def __repr__(self) -> str:
+        """Return string representation of this client."""
         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):
+    """Git client that uses urllib3 for HTTP(S) connections."""
+
     def __init__(
         self,
         base_url,
@@ -3273,6 +3406,7 @@ class Urllib3HttpGitClient(AbstractHttpGitClient):
         timeout=None,
         **kwargs,
     ) -> None:
+        """Initialize Urllib3HttpGitClient."""
         self._username = username
         self._password = password
         self._timeout = timeout
@@ -3389,6 +3523,7 @@ def get_transport_and_path_from_url(
       url: URL to open (a unicode string)
       config: Optional config object
       operation: Kind of operation that'll be performed; "pull" or "push"
+      **kwargs: Additional keyword arguments
 
     Keyword Args:
       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)
       config: Optional config object
       operation: Kind of operation that'll be performed; "pull" or "push"
+      **kwargs: Additional keyword arguments
 
     Keyword Args:
       thin_packs: Whether or not thin packs should be retrieved
@@ -3508,6 +3644,7 @@ DEFAULT_GIT_CREDENTIALS_PATHS = [
 def get_credentials_from_store(
     scheme, hostname, username=None, fnames=DEFAULT_GIT_CREDENTIALS_PATHS
 ):
+    """Read credentials from a Git credential store."""
     for fname in fnames:
         try:
             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):
+    """Object store implementation for Google Cloud Storage."""
+
     def __init__(self, bucket, subpath="") -> None:
+        """Initialize GcsObjectStore.
+
+        Args:
+          bucket: GCS bucket instance
+          subpath: Subpath within the bucket
+        """
         super().__init__()
         self.bucket = bucket
         self.subpath = subpath
 
     def __repr__(self) -> str:
+        """Return string representation of GcsObjectStore."""
         return f"{type(self).__name__}({self.bucket!r}, subpath={self.subpath!r})"
 
     def _remove_pack(self, name) -> None:

+ 22 - 0
dulwich/commit_graph.py

@@ -68,6 +68,15 @@ class CommitGraphEntry:
         generation: int,
         commit_time: int,
     ) -> 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.tree_id = tree_id
         self.parents = parents
@@ -75,6 +84,7 @@ class CommitGraphEntry:
         self.commit_time = commit_time
 
     def __repr__(self) -> str:
+        """Return string representation of CommitGraphEntry."""
         return (
             f"CommitGraphEntry(commit_id={self.commit_id!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."""
 
     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.data = data
 
     def __repr__(self) -> str:
+        """Return string representation of CommitGraphChunk."""
         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."""
 
     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.chunks: dict[bytes, CommitGraphChunk] = {}
         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:
+    """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)):
         return key.lower()
 
@@ -170,7 +181,18 @@ _T = TypeVar("_T")  # For get() default parameter
 
 
 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:
+        """Initialize a CaseInsensitiveOrderedMultiDict.
+
+        Args:
+          default_factory: Optional factory function for default values
+        """
         self._real: list[tuple[K, V]] = []
         self._keyed: dict[Any, V] = {}
         self._default_factory = default_factory
@@ -183,6 +205,18 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
         ] = None,
         default_factory: Optional[Callable[[], V]] = None,
     ) -> "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):
             return dict_in
 
@@ -200,14 +234,20 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
         return out
 
     def __len__(self) -> int:
+        """Return the number of unique keys in the dictionary."""
         return len(self._keyed)
 
     def keys(self) -> KeysView[K]:
+        """Return a view of the dictionary's keys."""
         return self._keyed.keys()  # type: ignore[return-value]
 
     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
         class OrderedItemsView(ItemsView[K, V]):
+            """Items view that preserves insertion order."""
+
             def __init__(self, mapping: CaseInsensitiveOrderedMultiDict[K, V]):
                 self._mapping = mapping
 
@@ -226,16 +266,25 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
         return OrderedItemsView(self)
 
     def __iter__(self) -> Iterator[K]:
+        """Iterate over the dictionary's keys."""
         return iter(self._keyed)
 
     def values(self) -> ValuesView[V]:
+        """Return a view of the dictionary's values."""
         return self._keyed.values()
 
     def __setitem__(self, key: K, value: V) -> None:
+        """Set a value for a key, appending to existing values."""
         self._real.append((key, value))
         self._keyed[lower_key(key)] = value
 
     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
         lower = lower_key(key)
         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
 
     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)
         del self._keyed[lower_k]
         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]
 
     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)]
 
     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:
             return self[key]
         except KeyError:
@@ -264,12 +332,32 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]):
                 return None
 
     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)
         for actual, value in self._real:
             if lower_key(actual) == lowered_key:
                 yield value
 
     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:
             return self[key]
         except KeyError:
@@ -338,6 +426,7 @@ class Config:
           section: Tuple with section name and optional subsection name
           name: Name of the setting, including section and possible
             subsection.
+          default: Default value if setting is not found
 
         Returns:
           Contents of the setting
@@ -414,29 +503,45 @@ class ConfigDict(Config):
         )
 
     def __repr__(self) -> str:
+        """Return string representation of ConfigDict."""
         return f"{self.__class__.__name__}({self._values!r})"
 
     def __eq__(self, other: object) -> bool:
+        """Check equality with another ConfigDict."""
         return isinstance(other, self.__class__) and other._values == self._values
 
     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)
 
     def __setitem__(
         self, key: Section, value: CaseInsensitiveOrderedMultiDict[Name, Value]
     ) -> None:
+        """Set configuration values for a section."""
         return self._values.__setitem__(key, value)
 
     def __delitem__(self, key: Section) -> None:
+        """Delete a configuration section.
+
+        Raises:
+          KeyError: If section not found
+        """
         return self._values.__delitem__(key)
 
     def __iter__(self) -> Iterator[Section]:
+        """Iterate over configuration sections."""
         return self._values.__iter__()
 
     def __len__(self) -> int:
+        """Return the number of sections."""
         return self._values.__len__()
 
     def keys(self) -> KeysView[Section]:
+        """Return a view of section names."""
         return self._values.keys()
 
     @classmethod
@@ -468,6 +573,15 @@ class ConfigDict(Config):
         return checked_section, name
 
     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)
 
         if len(section) > 1:
@@ -483,6 +597,18 @@ class ConfigDict(Config):
         section: SectionLike,
         name: NameLike,
     ) -> 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)
 
         if len(section) > 1:
@@ -499,6 +625,13 @@ class ConfigDict(Config):
         name: NameLike,
         value: Union[ValueLike, bool],
     ) -> None:
+        """Set a configuration value.
+
+        Args:
+            section: Section name
+            name: Setting name
+            value: Configuration value
+        """
         section, name = self._check_section_and_name(section, name)
 
         if isinstance(value, bool):
@@ -531,6 +664,7 @@ class ConfigDict(Config):
         self._values.setdefault(section)[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_dict = self._values.get(section_bytes)
         if section_dict is not None:
@@ -538,6 +672,7 @@ class ConfigDict(Config):
         return iter([])
 
     def sections(self) -> Iterator[Section]:
+        """Get all sections."""
         return iter(self._values.keys())
 
 
@@ -750,6 +885,12 @@ class ConfigFile(ConfigDict):
         ] = None,
         encoding: Union[str, 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)
         self.path: Optional[str] = None
         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:
+    """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.path.expanduser("~/.config/"),
@@ -1227,14 +1376,26 @@ class StackedConfig(Config):
     def __init__(
         self, backends: list[ConfigFile], writable: Optional[ConfigFile] = 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.writable = writable
 
     def __repr__(self) -> str:
+        """Return string representation of StackedConfig."""
         return f"<{self.__class__.__name__} for {self.backends!r}>"
 
     @classmethod
     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())
 
     @classmethod
@@ -1275,6 +1436,7 @@ class StackedConfig(Config):
         return backends
 
     def get(self, section: SectionLike, name: NameLike) -> Value:
+        """Get value from configuration."""
         if not isinstance(section, tuple):
             section = (section,)
         for backend in self.backends:
@@ -1285,6 +1447,7 @@ class StackedConfig(Config):
         raise KeyError(name)
 
     def get_multivar(self, section: SectionLike, name: NameLike) -> Iterator[Value]:
+        """Get multiple values from configuration."""
         if not isinstance(section, tuple):
             section = (section,)
         for backend in self.backends:
@@ -1296,11 +1459,13 @@ class StackedConfig(Config):
     def set(
         self, section: SectionLike, name: NameLike, value: Union[ValueLike, bool]
     ) -> None:
+        """Set value in configuration."""
         if self.writable is None:
             raise NotImplementedError(self.set)
         return self.writable.set(section, name, value)
 
     def sections(self) -> Iterator[Section]:
+        """Get all sections."""
         seen = set()
         for backend in self.backends:
             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
 # may not be encodable even to utf-8
 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:
       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:
+    """Main entry point for diffstat command line tool.
+
+    Returns:
+      Exit code (0 for success)
+    """
     argv = sys.argv
     # allow diffstat.py to also be used from the command line
     if len(sys.argv) > 1:

+ 55 - 0
dulwich/contrib/paramiko_vendor.py

@@ -41,7 +41,15 @@ import paramiko.config
 
 
 class _ParamikoWrapper:
+    """Wrapper for paramiko SSH channel to provide a file-like interface."""
+
     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.channel = channel
 
@@ -50,15 +58,38 @@ class _ParamikoWrapper:
 
     @property
     def stderr(self) -> BinaryIO:
+        """Get stderr stream from the channel.
+
+        Returns:
+            Binary IO stream for stderr
+        """
         return cast(BinaryIO, self.channel.makefile_stderr("rb"))
 
     def can_read(self) -> bool:
+        """Check if data is available to read.
+
+        Returns:
+            True if data is available
+        """
         return self.channel.recv_ready()
 
     def write(self, data: bytes) -> None:
+        """Write data to the channel.
+
+        Args:
+            data: Bytes to write
+        """
         return self.channel.sendall(data)
 
     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_len = len(data)
 
@@ -73,13 +104,21 @@ class _ParamikoWrapper:
         return data
 
     def close(self) -> None:
+        """Close the SSH channel."""
         self.channel.close()
 
 
 class ParamikoSSHVendor:
+    """SSH vendor implementation using paramiko."""
+
     # http://docs.paramiko.org/en/2.4/api/client.html
 
     def __init__(self, **kwargs: object) -> None:
+        """Initialize the paramiko SSH vendor.
+
+        Args:
+            **kwargs: Additional keyword arguments passed to SSHClient
+        """
         self.kwargs = kwargs
         self.ssh_config = self._load_ssh_config()
 
@@ -110,6 +149,22 @@ class ParamikoSSHVendor:
         protocol_version: Optional[int] = None,
         **kwargs: object,
     ) -> _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()
 
         # 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):
+    """HTTP Git client using the requests library."""
+
     def __init__(
         self,
         base_url: str,
@@ -58,6 +60,16 @@ class RequestsHttpGitClient(AbstractHttpGitClient):
         password: Optional[str] = None,
         **kwargs: object,
     ) -> 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._password = password
 
@@ -112,6 +124,14 @@ class RequestsHttpGitClient(AbstractHttpGitClient):
 
 
 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.headers.update({"Pragma": "no-cache"})
 

+ 126 - 4
dulwich/contrib/swift.py

@@ -97,7 +97,14 @@ cache_length = 20
 
 
 class PackInfoMissingObjectFinder(GreenThreadsMissingObjectFinder):
+    """Find missing objects required for pack generation."""
+
     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:
             if not self.objects_to_send:
                 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:
+    """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)
     info: dict = {}
     for obj in pack.iterobjects():
@@ -213,6 +229,16 @@ def load_pack_info(
     scon: Optional["SwiftConnector"] = None,
     file: Optional[BinaryIO] = None,
 ) -> 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 scon is None:
             return None
@@ -233,7 +259,7 @@ def load_pack_info(
 
 
 class SwiftException(Exception):
-    pass
+    """Exception raised for Swift-related errors."""
 
 
 class SwiftConnector:
@@ -281,6 +307,14 @@ class SwiftConnector:
         )
 
     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(";", ":")
         auth_httpclient = HTTPClient.from_url(
             self.auth_url,
@@ -304,6 +338,14 @@ class SwiftConnector:
         return storage_url, token
 
     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(";")
         auth_dict = {}
         auth_dict["auth"] = {
@@ -615,6 +657,14 @@ class SwiftPackData(PackData):
     def get_object_at(
         self, offset: int
     ) -> 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:
             return self._offset_cache[offset]
         assert offset >= self._header_size
@@ -625,11 +675,16 @@ class SwiftPackData(PackData):
         return (unpacked.pack_type_num, obj_data)
 
     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)
         return pack_reader.read_checksum()
 
     def close(self) -> None:
-        pass
+        """Close the pack data (no-op for Swift)."""
 
 
 class SwiftPack(Pack):
@@ -641,6 +696,12 @@ class SwiftPack(Pack):
     """
 
     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"]
         del kwargs["scon"]
         super().__init__(*args, **kwargs)  # type: ignore
@@ -698,6 +759,14 @@ class SwiftObjectStore(PackBasedObjectStore):
         return iter([])
 
     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:
             if sha in pack:
                 if hasattr(pack, "pack_info"):
@@ -748,6 +817,11 @@ class SwiftObjectStore(PackBasedObjectStore):
         f = BytesIO()
 
         def commit() -> Optional["SwiftPack"]:
+            """Commit the pack to Swift storage.
+
+            Returns:
+              The created SwiftPack or None if empty
+            """
             f.seek(0)
             pack = PackData(file=f, filename="")
             entries = pack.sorted_entries()
@@ -770,11 +844,16 @@ class SwiftObjectStore(PackBasedObjectStore):
                 return None
 
         def abort() -> None:
-            pass
+            """Abort the pack operation (no-op)."""
 
         return f, commit, abort
 
     def add_object(self, obj: object) -> None:
+        """Add a single object to the store.
+
+        Args:
+          obj: The object to add
+        """
         self.add_objects(
             [
                 (obj, None),  # type: ignore
@@ -871,6 +950,12 @@ class SwiftInfoRefsContainer(InfoRefsContainer):
     """Manage references in info/refs object."""
 
     def __init__(self, scon: SwiftConnector, store: object) -> None:
+        """Initialize SwiftInfoRefsContainer.
+
+        Args:
+          scon: Swift connector instance
+          store: Object store instance
+        """
         self.scon = scon
         self.filename = "info/refs"
         self.store = store
@@ -946,6 +1031,11 @@ class SwiftInfoRefsContainer(InfoRefsContainer):
         return True
 
     def allkeys(self) -> Iterator[bytes]:
+        """Get all reference names.
+
+        Returns:
+          Iterator of reference names as bytes
+        """
         try:
             self._refs[b"HEAD"] = self._refs[b"refs/heads/master"]
         except KeyError:
@@ -954,6 +1044,8 @@ class SwiftInfoRefsContainer(InfoRefsContainer):
 
 
 class SwiftRepo(BaseRepo):
+    """A Git repository backed by Swift object storage."""
+
     def __init__(self, root: str, conf: ConfigParser) -> None:
         """Init a Git bare Repository on top of a Swift container.
 
@@ -1020,17 +1112,37 @@ class SwiftRepo(BaseRepo):
 
 
 class SwiftSystemBackend(Backend):
+    """Backend for serving Git repositories from Swift."""
+
     def __init__(self, logger: "logging.Logger", conf: ConfigParser) -> None:
+        """Initialize SwiftSystemBackend.
+
+        Args:
+          logger: Logger instance
+          conf: Configuration parser instance
+        """
         self.conf = conf
         self.logger = logger
 
     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)
         return cast("BackendRepo", SwiftRepo(path, self.conf))
 
 
 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
 
     parser = optparse.OptionParser()
@@ -1082,6 +1194,11 @@ def cmd_daemon(args: list) -> None:
 
 
 def cmd_init(args: list) -> None:
+    """Initialize a new Git repository in Swift.
+
+    Args:
+      args: Command line arguments
+    """
     import optparse
 
     parser = optparse.OptionParser()
@@ -1103,6 +1220,11 @@ def cmd_init(args: list) -> None:
 
 
 def main(argv: list = sys.argv) -> None:
+    """Main entry point for Swift Git command line interface.
+
+    Args:
+      argv: Command line arguments
+    """
     commands = {
         "init": cmd_init,
         "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:
+    """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 = (
         url.scheme == url_prefix.scheme
         and url.hostname == url_prefix.hostname

+ 16 - 0
dulwich/diff_tree.py

@@ -54,10 +54,26 @@ class TreeChange(namedtuple("TreeChange", ["type", "old", "new"])):
 
     @classmethod
     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)
 
     @classmethod
     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)
 
 

+ 6 - 0
dulwich/dumb.py

@@ -410,6 +410,11 @@ class DumbRemoteHTTPRepo:
         return dict(self._refs)
 
     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_split = head_resp_bytes.replace(b"\n", b"").split(b" ")
         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)
           determine_wants: Function that returns list of wanted SHAs
           progress: Optional progress callback
+          get_tagged: Whether to get tagged objects
           depth: Depth for shallow clones (not supported for dumb HTTP)
 
         Returns:

+ 72 - 0
dulwich/errors.py

@@ -39,6 +39,13 @@ class ChecksumMismatch(Exception):
         got: Union[bytes, str],
         extra: Optional[str] = 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:
             expected_str = binascii.hexlify(expected).decode("ascii")
         else:
@@ -70,6 +77,13 @@ class WrongObjectException(Exception):
     type_name: str
 
     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}")
 
 
@@ -101,6 +115,13 @@ class MissingCommitError(Exception):
     """Indicates that a commit was not found in the repository."""
 
     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
         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."""
 
     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")
 
 
@@ -116,6 +144,12 @@ class ApplyDeltaError(Exception):
     """Indicates that applying a delta failed."""
 
     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)
 
 
@@ -123,6 +157,12 @@ class NotGitRepository(Exception):
     """Indicates that no Git repository was found."""
 
     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)
 
 
@@ -130,9 +170,23 @@ class GitProtocolError(Exception):
     """Git protocol exception."""
 
     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)
 
     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
 
 
@@ -144,6 +198,11 @@ class HangupException(GitProtocolError):
     """Hangup exception."""
 
     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:
             super().__init__(
                 "\n".join(
@@ -155,6 +214,14 @@ class HangupException(GitProtocolError):
         self.stderr_lines = stderr_lines
 
     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 (
             isinstance(other, HangupException)
             and self.stderr_lines == other.stderr_lines
@@ -165,6 +232,11 @@ class UnexpectedCommandError(GitProtocolError):
     """Unexpected command received in a proto line."""
 
     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}"
         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]:
+    """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
     # format_annotate_line.
     (name, email) = text.rsplit(b" <", 1)
@@ -50,12 +58,23 @@ class GitFastExporter:
     """Generate a fast-export output stream for Git objects."""
 
     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.store = store
         self.markers: dict[bytes, bytes] = {}
         self._marker_idx = 0
 
     def print_cmd(self, cmd: object) -> None:
+        """Print a command to the output stream.
+
+        Args:
+            cmd: Command object to print
+        """
         if hasattr(cmd, "__bytes__"):
             output = cmd.__bytes__()
         else:
@@ -63,15 +82,36 @@ class GitFastExporter:
         self.outf.write(output + b"\n")
 
     def _allocate_marker(self) -> bytes:
+        """Allocate a new marker.
+
+        Returns:
+            New marker as bytes
+        """
         self._marker_idx += 1
         return str(self._marker_idx).encode("ascii")
 
     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()
         self.markers[marker] = blob.id
         return (commands.BlobCommand(marker, blob.data), marker)
 
     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)
         self.print_cmd(cmd)
         return marker
@@ -137,6 +177,16 @@ class GitFastExporter:
     def emit_commit(
         self, commit: Commit, ref: Ref, base_tree: Optional[ObjectID] = None
     ) -> 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)
         self.print_cmd(cmd)
         return marker
@@ -154,6 +204,14 @@ class GitImportProcessor(processor.ImportProcessor):
         verbose: bool = False,
         outf: Optional[BinaryIO] = 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)
         self.repo = repo
         self.last_commit = ZERO_SHA
@@ -161,11 +219,27 @@ class GitImportProcessor(processor.ImportProcessor):
         self._contents: dict[bytes, tuple[int, bytes]] = {}
 
     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":"):
             return self.markers[objectish[1:]]
         return objectish
 
     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)
         self.process(p.iter_commands)
         return self.markers

+ 6 - 0
dulwich/file.py

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

+ 22 - 0
dulwich/filters.py

@@ -59,6 +59,14 @@ class ProcessFilterDriver:
         required: bool = False,
         cwd: Optional[str] = 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.smudge_cmd = smudge_cmd
         self.required = required
@@ -122,6 +130,12 @@ class FilterRegistry:
     def __init__(
         self, config: Optional["StackedConfig"] = None, repo: Optional["Repo"] = None
     ) -> None:
+        """Initialize FilterRegistry.
+
+        Args:
+          config: Git configuration stack
+          repo: Repository instance
+        """
         self.config = config
         self.repo = repo
         self._drivers: dict[str, FilterDriver] = {}
@@ -377,6 +391,14 @@ class FilterBlobNormalizer:
         filter_registry: Optional[FilterRegistry] = None,
         repo: Optional["Repo"] = 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.gitattributes = gitattributes
         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
 # liveable with integer time stamps using negation
 class WorkList(Generic[T]):
+    """Priority queue for commit processing using a min-heap."""
+
     def __init__(self) -> None:
+        """Initialize an empty work list."""
         self.pq: list[tuple[int, T]] = []
 
     def add(self, item: tuple[int, T]) -> None:
+        """Add an item to the work list.
+
+        Args:
+            item: Tuple of (timestamp, commit)
+        """
         dt, cmt = item
         heappush(self.pq, (-dt, cmt))
 
     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)
         if item:
             pr, cmt = item
@@ -52,6 +65,11 @@ class WorkList(Generic[T]):
         return None
 
     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:
             yield (-pr, cmt)
 
@@ -64,6 +82,19 @@ def _find_lcas(
     min_stamp: int = 0,
     shallows: Optional[set[ObjectID]] = None,
 ) -> 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 = []
     cstates = {}
 
@@ -74,6 +105,15 @@ def _find_lcas(
     _LCA = 8  # potential LCA (Lowest Common Ancestor)
 
     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():
             if cmt in cstates:
                 if not ((cstates[cmt] & _DNC) == _DNC):

+ 12 - 0
dulwich/greenthreads.py

@@ -89,6 +89,18 @@ class GreenThreadsMissingObjectFinder(MissingObjectFinder):
         concurrency: int = 1,
         get_parents: Optional[Callable[[ObjectID], list[ObjectID]]] = 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:
             self.sha_done.add(sha)
             obj = object_store[sha]

+ 33 - 0
dulwich/hooks.py

@@ -115,6 +115,12 @@ class PreCommitShellHook(ShellHook):
     """pre-commit shell hook."""
 
     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")
 
         ShellHook.__init__(self, "pre-commit", filepath, 0, cwd=cwd)
@@ -124,6 +130,11 @@ class PostCommitShellHook(ShellHook):
     """post-commit shell hook."""
 
     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")
 
         ShellHook.__init__(self, "post-commit", filepath, 0, cwd=controldir)
@@ -133,6 +144,11 @@ class CommitMsgShellHook(ShellHook):
     """commit-msg shell hook."""
 
     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")
 
         def prepare_msg(*args: bytes) -> tuple[str, ...]:
@@ -163,11 +179,28 @@ class PostReceiveShellHook(ShellHook):
     """post-receive shell hook."""
 
     def __init__(self, controldir: str) -> None:
+        """Initialize post-receive hook.
+
+        Args:
+            controldir: Path to the git control directory (.git)
+        """
         self.controldir = controldir
         filepath = os.path.join(controldir, "hooks", "post-receive")
         ShellHook.__init__(self, "post-receive", path=filepath, numparam=0)
 
     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
         if not os.path.exists(self.filepath):
             return None

+ 67 - 0
dulwich/ignore.py

@@ -308,6 +308,12 @@ class Pattern:
     """A single ignore pattern."""
 
     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.ignorecase = ignorecase
 
@@ -334,12 +340,30 @@ class Pattern:
         self._re = re.compile(translate(pattern), flags)
 
     def __bytes__(self) -> bytes:
+        """Return the pattern as bytes.
+
+        Returns:
+            The original pattern as bytes.
+        """
         return self.pattern
 
     def __str__(self) -> str:
+        """Return the pattern as a string.
+
+        Returns:
+            The pattern decoded as a string.
+        """
         return os.fsdecode(self.pattern)
 
     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 (
             isinstance(other, type(self))
             and self.pattern == other.pattern
@@ -347,6 +371,11 @@ class Pattern:
         )
 
     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})"
 
     def match(self, path: bytes) -> bool:
@@ -389,6 +418,13 @@ class IgnoreFilter:
         ignorecase: bool = False,
         path: Optional[str] = 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._ignorecase = ignorecase
         self._path = path
@@ -450,10 +486,20 @@ class IgnoreFilter:
     def from_path(
         cls, path: Union[str, os.PathLike], ignorecase: bool = False
     ) -> "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:
             return cls(read_ignore_patterns(f), ignorecase, path=str(path))
 
     def __repr__(self) -> str:
+        """Return string representation of IgnoreFilter."""
         path = getattr(self, "_path", None)
         if path is not None:
             return f"{type(self).__name__}.from_path({path!r})"
@@ -465,6 +511,11 @@ class IgnoreFilterStack:
     """Check for ignore status in multiple filters."""
 
     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
 
     def is_ignored(self, path: str) -> Optional[bool]:
@@ -482,6 +533,14 @@ class IgnoreFilterStack:
                 return status
         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:
     """Return default user ignore filter path.
@@ -514,12 +573,20 @@ class IgnoreFilterManager:
         global_filters: list[IgnoreFilter],
         ignorecase: bool,
     ) -> 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._top_path = top_path
         self._global_filters = global_filters
         self._ignorecase = ignorecase
 
     def __repr__(self) -> str:
+        """Return string representation of IgnoreFilterManager."""
         return f"{type(self).__name__}({self._top_path}, {self._global_filters!r}, {self._ignorecase!r})"
 
     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):
+    """Represents the stage of an index entry during merge conflicts."""
+
     NORMAL = 0
     MERGE_CONFLICT_ANCESTOR = 1
     MERGE_CONFLICT_THIS = 2
@@ -267,6 +269,12 @@ class Stage(Enum):
 
 @dataclass
 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
     ctime: Union[int, float, tuple[int, int]]
     mtime: Union[int, float, tuple[int, int]]
@@ -281,6 +289,11 @@ class SerializedIndexEntry:
     extended_flags: int
 
     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)
 
 
@@ -320,15 +333,33 @@ class TreeExtension(IndexExtension):
     """Tree cache extension."""
 
     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
         super().__init__(TREE_EXTENSION, b"")
 
     @classmethod
     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
         return cls([])
 
     def to_bytes(self) -> bytes:
+        """Serialize TreeExtension to bytes.
+
+        Returns:
+          Serialized extension data
+        """
         # TODO: Implement tree cache serialization
         return b""
 
@@ -337,15 +368,33 @@ class ResolveUndoExtension(IndexExtension):
     """Resolve undo extension for recording merge conflicts."""
 
     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
         super().__init__(REUC_EXTENSION, b"")
 
     @classmethod
     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
         return cls([])
 
     def to_bytes(self) -> bytes:
+        """Serialize ResolveUndoExtension to bytes.
+
+        Returns:
+          Serialized extension data
+        """
         # TODO: Implement resolve undo serialization
         return b""
 
@@ -354,15 +403,34 @@ class UntrackedExtension(IndexExtension):
     """Untracked cache extension."""
 
     def __init__(self, data: bytes) -> None:
+        """Initialize UntrackedExtension.
+
+        Args:
+            data: Raw untracked cache data
+        """
         super().__init__(UNTR_EXTENSION, data)
 
     @classmethod
     def from_bytes(cls, data: bytes) -> "UntrackedExtension":
+        """Parse UntrackedExtension from bytes.
+
+        Args:
+          data: Raw bytes to parse
+
+        Returns:
+          UntrackedExtension instance
+        """
         return cls(data)
 
 
 @dataclass
 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]]
     mtime: Union[int, float, tuple[int, int]]
     dev: int
@@ -377,6 +445,14 @@ class IndexEntry:
 
     @classmethod
     def from_serialized(cls, serialized: SerializedIndexEntry) -> "IndexEntry":
+        """Create an IndexEntry from a SerializedIndexEntry.
+
+        Args:
+          serialized: SerializedIndexEntry to convert
+
+        Returns:
+          New IndexEntry instance
+        """
         return cls(
             ctime=serialized.ctime,
             mtime=serialized.mtime,
@@ -392,6 +468,15 @@ class IndexEntry:
         )
 
     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.
         new_flags = self.flags & ~FLAG_STAGEMASK
         new_flags |= stage.value << FLAG_STAGESHIFT
@@ -411,6 +496,11 @@ class IndexEntry:
         )
 
     def stage(self) -> Stage:
+        """Get the merge conflict stage of this entry.
+
+        Returns:
+          Stage enum value
+        """
         return Stage((self.flags & FLAG_STAGEMASK) >> FLAG_STAGESHIFT)
 
     @property
@@ -420,6 +510,7 @@ class IndexEntry:
 
     def set_skip_worktree(self, skip: bool = True) -> None:
         """Helper method to set or clear the skip-worktree bit in extended_flags.
+
         Also sets FLAG_EXTENDED in self.flags if needed.
         """
         if skip:
@@ -448,6 +539,13 @@ class ConflictedIndexEntry:
         this: Optional[IndexEntry] = None,
         other: Optional[IndexEntry] = None,
     ) -> None:
+        """Initialize ConflictedIndexEntry.
+
+        Args:
+            ancestor: The common ancestor entry
+            this: The current branch entry
+            other: The other branch entry
+        """
         self.ancestor = ancestor
         self.this = this
         self.other = other
@@ -624,6 +722,11 @@ class UnsupportedIndexFormat(Exception):
     """An unsupported index format was encountered."""
 
     def __init__(self, version: int) -> None:
+        """Initialize UnsupportedIndexFormat exception.
+
+        Args:
+            version: The unsupported index format version
+        """
         self.index_format_version = version
 
 
@@ -740,6 +843,7 @@ def read_index_dict(
     f: BinaryIO,
 ) -> dict[bytes, Union[IndexEntry, ConflictedIndexEntry]]:
     """Read an index file and return it as a dictionary.
+
        Dict Key is tuple of path and stage number, as
             path alone is not unique
     Args:
@@ -811,6 +915,7 @@ def write_index_dict(
     extensions: Optional[list[IndexExtension]] = None,
 ) -> None:
     """Write an index file based on the contents of a dictionary.
+
     being careful to sort by path and then by stage.
     """
     entries_list = []
@@ -889,9 +994,15 @@ class Index:
 
     @property
     def path(self) -> Union[bytes, str]:
+        """Get the path to the index file.
+
+        Returns:
+          Path to the index file
+        """
         return self._filename
 
     def __repr__(self) -> str:
+        """Return string representation of Index."""
         return f"{self.__class__.__name__}({self._filename!r})"
 
     def write(self) -> None:
@@ -967,6 +1078,7 @@ class Index:
         return iter(self._byname)
 
     def __contains__(self, key: bytes) -> bool:
+        """Check if a path exists in the index."""
         return key in self._byname
 
     def get_sha1(self, path: bytes) -> bytes:
@@ -992,6 +1104,11 @@ class Index:
             yield path, entry.sha, cleanup_mode(entry.mode)
 
     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():
             if isinstance(value, ConflictedIndexEntry):
                 return True
@@ -1004,27 +1121,49 @@ class Index:
     def __setitem__(
         self, name: bytes, value: Union[IndexEntry, ConflictedIndexEntry]
     ) -> None:
+        """Set an entry in the index."""
         assert isinstance(name, bytes)
         self._byname[name] = value
 
     def __delitem__(self, name: bytes) -> None:
+        """Delete an entry from the index."""
         del self._byname[name]
 
     def iteritems(
         self,
     ) -> 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())
 
     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())
 
     def update(
         self, entries: dict[bytes, Union[IndexEntry, ConflictedIndexEntry]]
     ) -> None:
+        """Update the index with multiple entries.
+
+        Args:
+          entries: Dictionary mapping paths to index entries
+        """
         for key, value in entries.items():
             self[key] = value
 
     def paths(self) -> Generator[bytes, None, None]:
+        """Generate all paths in the index.
+
+        Yields:
+          Path names as bytes
+        """
         yield from self._byname.keys()
 
     def changes_from_tree(
@@ -1147,8 +1286,7 @@ def changes_from_tree(
         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:
       names: Iterable of names in the working copy
@@ -1194,6 +1332,7 @@ def index_entry_from_stat(
     Args:
       stat_val: POSIX stat_result instance
       hex_sha: Hex sha of the object
+      mode: Optional file mode, will be derived from stat if not provided
     """
     if mode is None:
         mode = cleanup_mode(stat_val.st_mode)
@@ -1221,7 +1360,14 @@ if sys.platform == "win32":
     # https://github.com/jelmer/dulwich/issues/1005
 
     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:
+            """Initialize WindowsSymlinkPermissionError."""
             super(PermissionError, self).__init__(
                 errno,
                 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,
     ) -> 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:
             return os.symlink(
                 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
       honor_filemode: An optional flag to honor core.filemode setting in
         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
     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:
+    """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
 
 
 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)
     if normalized in INVALID_DOTNAMES:
         return False
@@ -1437,6 +1611,7 @@ def build_index_from_tree(
         config file, default is core.filemode=True, change executable bit
       validate_path_element: Function to validate path elements to check
         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
         endings when writing blobs to the working directory.
       tree_encoding: Encoding used for tree paths (default: utf-8)
@@ -1510,6 +1685,7 @@ def blob_from_path_and_mode(
     Args:
       fs_path: Full file system path to file
       mode: File mode
+      tree_encoding: Encoding to use for tree contents
     Returns: A `Blob` object
     """
     assert isinstance(fs_path, bytes)
@@ -1534,6 +1710,7 @@ def blob_from_path_and_stat(
     Args:
       fs_path: Full file system path to file
       st: A stat object
+      tree_encoding: Encoding to use for tree contents
     Returns: A `Blob` object
     """
     return blob_from_path_and_mode(fs_path, st.st_mode, tree_encoding)
@@ -2269,6 +2446,7 @@ def get_unstaged_changes(
     Args:
       index: index to check
       root_path: path in which to find files
+      filter_blob_callback: Optional callback to filter blobs
     Returns: iterator over paths with unstaged changes
     """
     # 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]:
+    """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")):
         head = read_submodule_head(path)
         if head is None:
@@ -2436,6 +2625,7 @@ def iter_fresh_objects(
     """Iterate over versions of objects on disk referenced by index.
 
     Args:
+      paths: Paths to check
       root_path: Root path to access from
       include_deleted: Include deleted entries with sha and
         mode set to None
@@ -2473,9 +2663,11 @@ class locked_index:
     _file: "_GitFile"
 
     def __init__(self, path: Union[bytes, str]) -> None:
+        """Initialize locked_index."""
         self._path = path
 
     def __enter__(self) -> Index:
+        """Enter context manager and lock index."""
         f = GitFile(self._path, "wb")
         assert isinstance(f, _GitFile)  # GitFile in write mode always returns _GitFile
         self._file = f
@@ -2488,6 +2680,7 @@ class locked_index:
         exc_value: Optional[BaseException],
         traceback: Optional[types.TracebackType],
     ) -> None:
+        """Exit context manager and unlock index."""
         if exc_type is not None:
             self._file.abort()
             return

+ 6 - 0
dulwich/lfs.py

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

+ 7 - 0
dulwich/lfs_server.py

@@ -246,6 +246,13 @@ class LFSServer(HTTPServer):
         lfs_store: LFSStore,
         log_requests: bool = False,
     ) -> 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)
         self.lfs_store = lfs_store
         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,
         binary_detection: bool = True,
     ):
+        """Initialize LineEndingFilter."""
         self.clean_conversion = clean_conversion
         self.smudge_conversion = smudge_conversion
         self.binary_detection = binary_detection
@@ -323,8 +324,7 @@ def get_checkin_filter_autocrlf(
 
 
 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.
     """
@@ -336,6 +336,7 @@ class BlobNormalizer(FilterBlobNormalizer):
         core_eol: str = "native",
         autocrlf: bytes = b"false",
     ) -> None:
+        """Initialize FilteringBlobNormalizer."""
         # Set up a filter registry with line ending filters
         filter_registry = FilterRegistry(config_stack)
 
@@ -432,10 +433,7 @@ class BlobNormalizer(FilterBlobNormalizer):
 def normalize_blob(
     blob: Blob, conversion: Callable[[bytes], bytes], binary_detection: bool
 ) -> 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
     data = blob.data
 
@@ -456,6 +454,8 @@ def normalize_blob(
 
 
 class TreeBlobNormalizer(BlobNormalizer):
+    """Blob normalizer that tracks existing files in a tree."""
+
     def __init__(
         self,
         config_stack: "StackedConfig",
@@ -465,6 +465,7 @@ class TreeBlobNormalizer(BlobNormalizer):
         core_eol: str = "native",
         autocrlf: bytes = b"false",
     ) -> None:
+        """Initialize TreeBlobNormalizer."""
         super().__init__(config_stack, git_attributes, core_eol, autocrlf)
         if tree:
             self.existing_paths = {
@@ -474,6 +475,7 @@ class TreeBlobNormalizer(BlobNormalizer):
             self.existing_paths = set()
 
     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
         # previously normalized on checkout
         if (

+ 18 - 0
dulwich/lru_cache.py

@@ -78,6 +78,12 @@ class LRUCache(Generic[K, V]):
     def __init__(
         self, max_cache: int = 100, after_cleanup_count: Optional[int] = 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]] = {}
         # The "HEAD" of the lru linked list
         self._most_recently_used = None
@@ -86,9 +92,11 @@ class LRUCache(Generic[K, V]):
         self._update_max_cache(max_cache, after_cleanup_count)
 
     def __contains__(self, key: K) -> bool:
+        """Check if key is in cache."""
         return key in self._cache
 
     def __getitem__(self, key: K) -> V:
+        """Get item from cache and mark as recently used."""
         cache = self._cache
         node = cache[key]
         # Inlined from _record_access to decrease the overhead of __getitem__
@@ -122,6 +130,7 @@ class LRUCache(Generic[K, V]):
         return node.value
 
     def __len__(self) -> int:
+        """Return number of items in cache."""
         return len(self._cache)
 
     def _walk_lru(self) -> Iterator[_LRUNode[K, V]]:
@@ -196,6 +205,15 @@ class LRUCache(Generic[K, V]):
         return self._max_cache
 
     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)
         if node is None:
             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]]:
+    """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
     # dulwich.repo.check_user_identity
     (name_str, email_str) = text.rsplit(b"<", 1)
@@ -81,6 +89,11 @@ class Mailmap:
             ]
         ] = None,
     ) -> None:
+        """Initialize Mailmap.
+
+        Args:
+          map: Optional iterator of (canonical_identity, from_identity) tuples
+        """
         self._table: dict[
             tuple[Optional[bytes], Optional[bytes]],
             tuple[Optional[bytes], Optional[bytes]],
@@ -143,5 +156,13 @@ class Mailmap:
 
     @classmethod
     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:
             return cls(read_mailmap(f))

+ 6 - 0
dulwich/merge.py

@@ -18,6 +18,12 @@ class MergeConflict(Exception):
     """Raised when a merge conflict occurs."""
 
     def __init__(self, path: bytes, message: str) -> None:
+        """Initialize MergeConflict.
+
+        Args:
+          path: Path to the conflicted file
+          message: Conflict description
+        """
         self.path = path
         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
     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
         for name, mode, sha in tree.items():
             if stat.S_ISREG(mode):
@@ -223,6 +232,16 @@ class NotesTree:
 
             # Build new tree structure
             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:
                     # Leaf level - add the note blob
                     new_tree = Tree()
@@ -368,6 +387,16 @@ class NotesTree:
 
         # Build new tree structure
         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:
                 # Leaf level - add the note blob
                 new_tree = Tree()
@@ -429,6 +458,15 @@ class NotesTree:
 
         # Build new tree structure without the note
         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:
                 # Leaf level - remove the note
                 new_tree = Tree()
@@ -484,6 +522,15 @@ class NotesTree:
         """
 
         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():
                 if stat.S_ISDIR(mode):  # Directory
                     subtree = self._object_store[sha]

+ 21 - 0
dulwich/object_store.py

@@ -156,10 +156,12 @@ def get_depth(
     max_depth=None,
 ):
     """Return the current available depth for the given head.
+
     For commits with multiple parents, the largest possible depth will be
     returned.
 
     Args:
+        store: Object store to search in
         head: commit to start from
         get_parents: optional function for getting the parents of a commit
         max_depth: maximum depth to search
@@ -272,6 +274,7 @@ class BaseObjectStore:
 
         Args:
           objects: Iterable over a list of (object, path) tuples
+          progress: Optional progress callback
         """
         raise NotImplementedError(self.add_objects)
 
@@ -455,6 +458,7 @@ class BaseObjectStore:
         max_depth=None,
     ):
         """Return the current available depth for the given head.
+
         For commits with multiple parents, the largest possible depth will be
         returned.
 
@@ -583,6 +587,8 @@ class PackBasedObjectStore(BaseObjectStore, PackedObjectContainer):
 
         Args:
           count: Number of items to add
+          unpacked_objects: Iterator of UnpackedObject instances
+          progress: Optional progress callback
         """
         if count == 0:
             # Don't bother writing an empty pack file
@@ -950,6 +956,7 @@ class PackBasedObjectStore(BaseObjectStore, PackedObjectContainer):
 
         Args:
           sha1: sha for the object.
+          include_comp: Whether to include compression metadata.
         """
         if sha1 == ZERO_SHA:
             raise KeyError(sha1)
@@ -992,6 +999,7 @@ class PackBasedObjectStore(BaseObjectStore, PackedObjectContainer):
         Args:
           objects: Iterable over (object, path) tuples, should support
             __len__.
+          progress: Optional progress reporting function.
         Returns: Pack object of the objects written.
         """
         count = len(objects)
@@ -1351,7 +1359,9 @@ class DiskObjectStore(PackBasedObjectStore):
         Args:
           f: Open file object for the pack.
           path: Path to the pack file.
+          num_objects: Number of objects in the pack.
           indexer: A PackIndexer for indexing the pack.
+          progress: Optional progress reporting function.
         """
         entries = []
         for i, entry in enumerate(indexer):
@@ -1426,6 +1436,7 @@ class DiskObjectStore(PackBasedObjectStore):
             requested bytes are read.
           read_some: Read function that returns at least one byte, but may
             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
             objects/pack directory.
         """
@@ -1765,6 +1776,7 @@ class MemoryObjectStore(BaseObjectStore):
 
         Args:
           objects: Iterable over a list of (object, path) tuples
+          progress: Optional progress reporting function.
         """
         for obj, path in objects:
             self.add_object(obj)
@@ -1806,6 +1818,8 @@ class MemoryObjectStore(BaseObjectStore):
 
         Args:
           count: Number of items to add
+          unpacked_objects: Iterator of UnpackedObject instances
+          progress: Optional progress reporting function.
         """
         if count == 0:
             return
@@ -1839,6 +1853,7 @@ class MemoryObjectStore(BaseObjectStore):
             requested bytes are read.
           read_some: Read function that returns at least one byte, but may
             not return the number of bytes requested.
+          progress: Optional progress reporting function.
         """
         f, commit, abort = self.add_pack()
         try:
@@ -2136,6 +2151,8 @@ class ObjectStoreGraphWalker:
         Args:
           local_heads: Heads to start search with
           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.get_parents = get_parents
@@ -2533,9 +2550,11 @@ def _collect_ancestors(
     """Collect all ancestors of heads up to (excluding) those in common.
 
     Args:
+      store: Object store to get commits from
       heads: commits to start from
       common: commits to end at, or empty set to walk repository
         completely
+      shallow: Set of shallow commits
       get_parents: Optional function for getting the parents of a
         commit.
     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.
 
     Args:
+      store: Object store to get trees from
       tree_id: SHA1 of the tree.
       include_trees: If True, include tree objects in the iteration.
     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.
 
     Args:
+      store: Object store to get objects from
       sha: The object SHA to peel.
     Returns: The fully-peeled SHA1 of a tag object, after peeling all
         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:
+    """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:
         return False
     try:
@@ -185,10 +193,24 @@ def serializable_property(name: str, docstring: Optional[str] = None) -> propert
     """A property that helps tracking whether serialization is necessary."""
 
     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)
         obj._needs_serialization = True
 
     def get(obj: "ShaFile") -> object:
+        """Get the property value.
+
+        Args:
+          obj: The ShaFile object
+
+        Returns:
+          The property value
+        """
         return getattr(obj, "_" + name)
 
     return property(get, set, doc=docstring)
@@ -276,6 +298,11 @@ class FixedSha:
     __slots__ = ("_hexsha", "_sha")
 
     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):
             hexsha = hexsha.encode("ascii")
         if not isinstance(hexsha, bytes):
@@ -623,6 +650,7 @@ class ShaFile:
         return self.sha().hexdigest().encode("ascii")
 
     def __repr__(self) -> str:
+        """Return string representation of this object."""
         return f"<{self.__class__.__name__} {self.id!r}>"
 
     def __ne__(self, other: object) -> bool:
@@ -657,6 +685,7 @@ class Blob(ShaFile):
     _chunked_text: list[bytes]
 
     def __init__(self) -> None:
+        """Initialize a new Blob object."""
         super().__init__()
         self._chunked_text = []
         self._needs_serialization = False
@@ -691,6 +720,17 @@ class Blob(ShaFile):
 
     @classmethod
     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)
         if not isinstance(blob, cls):
             raise NotBlobError(_path_to_bytes(path))
@@ -830,6 +870,7 @@ class Tag(ShaFile):
     _tagger: Optional[bytes]
 
     def __init__(self) -> None:
+        """Initialize a new Tag object."""
         super().__init__()
         self._tagger = None
         self._tag_time = None
@@ -839,6 +880,17 @@ class Tag(ShaFile):
 
     @classmethod
     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)
         if not isinstance(tag, cls):
             raise NotTagError(_path_to_bytes(filename))
@@ -991,6 +1043,12 @@ class Tag(ShaFile):
     signature = serializable_property("signature", "Optional detached GPG signature")
 
     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
 
         with gpg.Context(armor=True) as c:
@@ -1065,6 +1123,7 @@ def parse_tree(text: bytes, strict: bool = False) -> Iterator[tuple[bytes, int,
 
     Args:
       text: Serialized text to parse
+      strict: If True, enforce strict validation
     Returns: iterator of tuples of (name, mode, sha)
 
     Raises:
@@ -1155,6 +1214,7 @@ def pretty_format_tree_entry(
       name: Name of the directory entry
       mode: Mode of entry
       hexsha: Hexsha of the referenced object
+      encoding: Character encoding for the name
     Returns: string describing the tree entry
     """
     if mode & stat.S_IFDIR:
@@ -1173,6 +1233,12 @@ class SubmoduleEncountered(Exception):
     """A submodule was encountered while resolving a path."""
 
     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.sha = sha
 
@@ -1186,20 +1252,34 @@ class Tree(ShaFile):
     __slots__ = "_entries"
 
     def __init__(self) -> None:
+        """Initialize an empty Tree."""
         super().__init__()
         self._entries: dict[bytes, tuple[int, bytes]] = {}
 
     @classmethod
     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)
         if not isinstance(tree, cls):
             raise NotTreeError(_path_to_bytes(filename))
         return tree
 
     def __contains__(self, name: bytes) -> bool:
+        """Check if name exists in tree."""
         return name in self._entries
 
     def __getitem__(self, name: bytes) -> tuple[int, ObjectID]:
+        """Get tree entry by name."""
         return self._entries[name]
 
     def __setitem__(self, name: bytes, value: tuple[int, ObjectID]) -> None:
@@ -1216,13 +1296,16 @@ class Tree(ShaFile):
         self._needs_serialization = True
 
     def __delitem__(self, name: bytes) -> None:
+        """Delete tree entry by name."""
         del self._entries[name]
         self._needs_serialization = True
 
     def __len__(self) -> int:
+        """Return number of entries in tree."""
         return len(self._entries)
 
     def __iter__(self) -> Iterator[bytes]:
+        """Iterate over tree entry names."""
         return iter(self._entries)
 
     def add(self, name: bytes, mode: int, hexsha: bytes) -> None:
@@ -1305,6 +1388,11 @@ class Tree(ShaFile):
         return list(serialize_tree(self.iteritems()))
 
     def as_pretty_string(self) -> str:
+        """Return a human-readable string representation of this tree.
+
+        Returns:
+          Pretty-printed tree entries
+        """
         text: list[str] = []
         for name, mode, hexsha in self.iteritems():
             text.append(pretty_format_tree_entry(name, mode, hexsha))
@@ -1530,6 +1618,7 @@ class Commit(ShaFile):
     )
 
     def __init__(self) -> None:
+        """Initialize an empty Commit."""
         super().__init__()
         self._parents: list[bytes] = []
         self._encoding: Optional[bytes] = None
@@ -1541,6 +1630,17 @@ class Commit(ShaFile):
 
     @classmethod
     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)
         if not isinstance(commit, cls):
             raise NotCommitError(_path_to_bytes(path))
@@ -1653,6 +1753,12 @@ class Commit(ShaFile):
         # TODO: optionally check for duplicate parents
 
     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
 
         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:
+    """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:
         text = text.encode("ascii")  # type: ignore
     return text  # type: ignore
@@ -233,6 +241,7 @@ def parse_reftuple(
       lh_container: A RefsContainer object
       rh_container: A RefsContainer object
       refspec: A string
+      force: Whether to force the operation
     Returns: A tuple with left and right ref
     Raises:
       KeyError: If one of the refs can not be found
@@ -348,6 +357,12 @@ class AmbiguousShortId(Exception):
     """The short id is ambiguous."""
 
     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.options = options
 

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 437 - 4
dulwich/pack.py


+ 48 - 2
dulwich/patch.py

@@ -58,8 +58,12 @@ def write_commit_patch(
     """Write a individual file patch.
 
     Args:
+      f: File-like object to write to
       commit: Commit object
+      contents: Contents of the patch
       progress: tuple with current patch number and total.
+      version: Version string to include in patch header
+      encoding: Encoding to use for the patch
 
     Returns:
       tuple with filename and contents
@@ -147,8 +151,7 @@ def unified_diff(
     tree_encoding: str = "utf-8",
     output_encoding: str = "utf-8",
 ) -> 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
     """
@@ -197,6 +200,14 @@ def is_binary(content: bytes) -> bool:
 
 
 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:
         return b"0" * 7
     else:
@@ -204,6 +215,15 @@ def shortid(hexsha: Optional[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:
         return b"/dev/null"
     else:
@@ -235,6 +255,15 @@ def write_object_diff(
     patched_new_path = patch_filename(new_path, b"b")
 
     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
 
         if hexsha is None:
@@ -250,6 +279,14 @@ def write_object_diff(
                 return cast(Blob, Blob.from_string(obj.as_raw_string()))
 
     def lines(content: "Blob") -> list[bytes]:
+        """Split blob content into lines.
+
+        Args:
+            content: Blob content
+
+        Returns:
+            List of lines
+        """
         if not content:
             return []
         else:
@@ -338,6 +375,14 @@ def write_blob_diff(
     patched_new_path = patch_filename(new_path, b"b")
 
     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:
             return blob.splitlines()
         else:
@@ -368,6 +413,7 @@ def write_tree_diff(
 
     Args:
       f: File-like object to write to.
+      store: Object store to read from
       old_tree: Old tree id
       new_tree: New tree id
       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():
-    """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.
     """
     local_timezone = time.localtime().tm_gmtoff
@@ -391,6 +391,7 @@ def open_repo_closing(
     path_or_repo: Union[str, os.PathLike, T],
 ) -> AbstractContextManager[Union[T, Repo]]:
     """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
     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(
     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:
       repopath: Repository 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
     """
     # Resolve might returns a relative path on Windows
@@ -570,6 +571,7 @@ def commit(
       author_timezone: Author timestamp timezone
       committer: Optional committer name and email
       commit_timezone: Commit timestamp timezone
+      encoding: Encoding to use for commit message
       no_verify: Skip pre-commit and commit-msg hooks
       signoff: GPG Sign the commit (bool, defaults to False,
         pass True to use default GPG key,
@@ -664,6 +666,7 @@ def commit_tree(
     Args:
       repo: Path to repository
       tree: An existing tree object
+      message: Commit message
       author: Optional author 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
         mutually supported protocol version will be used.
       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
     """
@@ -977,6 +978,7 @@ def remove(repo: Union[str, os.PathLike, Repo] = ".", paths=None, cached=False)
     Args:
       repo: Repository for the files
       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:
         index = r.open_index()
@@ -1180,6 +1182,7 @@ def print_commit(commit, decode, outstream=sys.stdout) -> None:
 
     Args:
       commit: A `Commit` object
+      decode: Function to decode commit data
       outstream: A stream file to write to
     """
     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
     class _StreamWrapper:
+        """Wrapper for ColorizedDiffStream to handle string/bytes conversion."""
+
         def __init__(self, stream):
+            """Initialize a _StreamWrapper.
+
+            Args:
+              stream: The underlying stream to wrap
+            """
             self.stream = stream
 
         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):
                 # Convert string to bytes for ColorizedDiffStream
                 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.
       init: If True, initialize submodules first
       force: Force update even if local changes exist
+      errstream: Error stream for error messages
     """
     from .submodule import iter_cached_submodules
 
@@ -1781,6 +1797,7 @@ def tag_create(
       sign: GPG Sign the tag (bool, defaults to False,
         pass True to use default 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:
         object = parse_object(r, objectish)
@@ -2173,6 +2190,7 @@ def push(
       outstream: A stream file to write output
       errstream: A stream file to write errors
       force: Force overwriting refs
+      **kwargs: Additional keyword arguments for the client
     """
     # Open the repo
     with open_repo_closing(repo) as r:
@@ -2301,6 +2319,7 @@ def pull(
         feature, and ignored otherwise.
       protocol_version: desired Git protocol version. By default the highest
         mutually supported protocol version will be used
+      **kwargs: Additional keyword arguments for the client
     """
     # Open the repo
     with open_repo_closing(repo) as r:
@@ -3019,6 +3038,8 @@ def fetch(
       depth: Depth to fetch at
       prune: Prune remote removed refs
       prune_tags: Prune reomte removed tags
+      force: Force fetching even if it would overwrite local changes
+      **kwargs: Additional keyword arguments for the client
     Returns:
       Dictionary with refs on the remote
     """
@@ -3108,6 +3129,7 @@ def ls_remote(remote, config: Optional[Config] = None, **kwargs):
     Args:
       remote: Remote repository location
       config: Configuration to use
+      **kwargs: Additional keyword arguments for the client
     Returns:
       LsRemoteResult object with refs and symrefs
     """
@@ -3616,6 +3638,7 @@ def reset_file(
       repo: dulwich Repo object
       file_path: file to reset, relative to the repository path
       target: branch or commit or b'HEAD' to reset
+      symlink_fn: Function to use for creating symlinks
     """
     tree = parse_tree(repo, treeish=target)
     tree_path = _fs_to_tree_path(file_path)
@@ -4249,7 +4272,7 @@ def merge_tree(
         return merged_tree.id, conflicts
 
 
-def cherry_pick(
+def cherry_pick(  # noqa: D417
     repo: Union[str, os.PathLike, Repo],
     committish: Union[str, bytes, Commit, Tag, None],
     no_commit=False,
@@ -4260,9 +4283,9 @@ def cherry_pick(
 
     Args:
       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
-      ``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
 
     Returns:

+ 144 - 0
dulwich/protocol.py

@@ -131,22 +131,57 @@ NAK_LINE = b"NAK\n"
 
 
 def agent_string() -> bytes:
+    """Generate the agent string for dulwich.
+
+    Returns:
+      Agent string as bytes
+    """
     return ("dulwich/" + ".".join(map(str, dulwich.__version__))).encode("ascii")
 
 
 def capability_agent() -> bytes:
+    """Generate the agent capability string.
+
+    Returns:
+      Agent capability with dulwich version
+    """
     return CAPABILITY_AGENT + b"=" + agent_string()
 
 
 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
 
 
 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}
 
 
 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)
     if len(parts) == 1:
         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]:
+    """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]
 
 
@@ -166,10 +209,27 @@ COMMAND_HAVE = b"have"
 
 
 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])
 
 
 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" ")
     cmd, args = line[:splice_at], line[splice_at + 1 :]
     assert args[-1:] == b"\x00"
@@ -229,6 +289,14 @@ class Protocol:
         close: Optional[Callable[[], None]] = None,
         report_activity: Optional[Callable[[int, str], 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.write = write
         self._close = close
@@ -236,10 +304,12 @@ class Protocol:
         self._readahead: Optional[BytesIO] = None
 
     def close(self) -> None:
+        """Close the underlying transport if a close function was provided."""
         if self._close:
             self._close()
 
     def __enter__(self) -> "Protocol":
+        """Enter context manager."""
         return self
 
     def __exit__(
@@ -248,6 +318,7 @@ class Protocol:
         exc_val: Optional[BaseException],
         exc_tb: Optional[types.TracebackType],
     ) -> None:
+        """Exit context manager and close transport."""
         self.close()
 
     def read_pkt_line(self) -> Optional[bytes]:
@@ -405,12 +476,29 @@ class ReceivableProtocol(Protocol):
         report_activity: Optional[Callable[[int, str], None]] = None,
         rbufsize: int = _RBUFSIZE,
     ) -> 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)
         self._recv = recv
         self._rbuf = BytesIO()
         self._rbufsize = rbufsize
 
     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,
         # with the following modifications:
         #  - omit the size <= 0 branch
@@ -472,6 +560,14 @@ class ReceivableProtocol(Protocol):
         return buf.read()
 
     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
 
         buf = self._rbuf
@@ -585,6 +681,11 @@ class PktLineParser:
     """Packet line parser that hands completed packets off to a callback."""
 
     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._readahead = BytesIO()
 
@@ -613,12 +714,30 @@ class PktLineParser:
 
 
 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])
 
 
 def format_ref_line(
     ref: bytes, sha: bytes, capabilities: Optional[list[bytes]] = None
 ) -> 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:
         return sha + b" " + ref + b"\n"
     else:
@@ -626,14 +745,39 @@ def format_ref_line(
 
 
 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
 
 
 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
 
 
 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:
         ack_type = b" " + ack_type
     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."""
 
     def __init__(self, conflicted_files: list[bytes]):
+        """Initialize RebaseConflict.
+
+        Args:
+          conflicted_files: List of conflicted file paths
+        """
         self.conflicted_files = conflicted_files
         super().__init__(
             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."""
 
     def __init__(self, repo: Repo) -> None:
+        """Initialize MemoryRebaseStateManager.
+
+        Args:
+          repo: Repository instance
+        """
         self.repo = repo
         self._state: Optional[dict] = 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."""
 
     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.depth = depth
 
@@ -137,6 +143,11 @@ class RefsContainer:
     """A container for refs."""
 
     def __init__(self, logger=None) -> None:
+        """Initialize a RefsContainer.
+
+        Args:
+          logger: Optional logger for reflog updates
+        """
         self._logger = logger
 
     def _log(
@@ -169,6 +180,9 @@ class RefsContainer:
         Args:
           name: Name of the ref to set
           other: Name of the ref to point at
+          committer: Optional committer name/email
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Optional message
         """
         raise NotImplementedError(self.set_symbolic_ref)
@@ -213,6 +227,17 @@ class RefsContainer:
         message: Optional[bytes] = None,
         prune: bool = False,
     ) -> 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:
             to_delete = set(self.subkeys(base))
         else:
@@ -237,6 +262,7 @@ class RefsContainer:
         raise NotImplementedError(self.allkeys)
 
     def __iter__(self):
+        """Iterate over all ref names."""
         return iter(self.allkeys())
 
     def keys(self, base=None):
@@ -346,6 +372,14 @@ class RefsContainer:
         return refnames, contents
 
     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):
             return True
         return False
@@ -381,6 +415,9 @@ class RefsContainer:
           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/email
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Message for reflog
         Returns: True if the set was successful, False otherwise.
         """
@@ -394,6 +431,10 @@ class RefsContainer:
         Args:
           name: Ref name
           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)
 
@@ -434,6 +475,9 @@ class RefsContainer:
           name: The refname to delete.
           old_ref: The old sha the refname must refer to, or None to
             delete unconditionally.
+          committer: Optional committer name/email
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Message for reflog
         Returns: True if the delete was successful, False otherwise.
         """
@@ -486,18 +530,37 @@ class DictRefsContainer(RefsContainer):
     """
 
     def __init__(self, refs, logger=None) -> None:
+        """Initialize DictRefsContainer."""
         super().__init__(logger=logger)
         self._refs = refs
         self._peeled: dict[bytes, ObjectID] = {}
         self._watchers: set[Any] = set()
 
     def allkeys(self):
+        """Get all ref names.
+
+        Returns:
+          All ref names in the container
+        """
         return self._refs.keys()
 
     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)
 
     def get_packed_refs(self):
+        """Get packed refs (always empty for DictRefsContainer).
+
+        Returns:
+          Empty dictionary
+        """
         return {}
 
     def _notify(self, ref, newsha) -> None:
@@ -513,6 +576,16 @@ class DictRefsContainer(RefsContainer):
         timezone=None,
         message=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]
         new = SYMREF + other
         self._refs[name] = new
@@ -537,6 +610,24 @@ class DictRefsContainer(RefsContainer):
         timezone=None,
         message=None,
     ) -> 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:
             return False
         # Only update the specific ref requested, not the whole chain
@@ -564,6 +655,19 @@ class DictRefsContainer(RefsContainer):
         timezone=None,
         message: Optional[bytes] = None,
     ) -> 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:
             return False
         self._refs[name] = ref
@@ -588,6 +692,23 @@ class DictRefsContainer(RefsContainer):
         timezone=None,
         message=None,
     ) -> 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:
             return False
         try:
@@ -608,6 +729,14 @@ class DictRefsContainer(RefsContainer):
         return True
 
     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)
 
     def _update(self, refs) -> None:
@@ -626,21 +755,55 @@ class InfoRefsContainer(RefsContainer):
     """Refs container that reads refs from a info/refs file."""
 
     def __init__(self, f) -> None:
+        """Initialize an InfoRefsContainer.
+
+        Args:
+          f: File-like object containing info/refs data
+        """
         self._refs = {}
         self._peeled = {}
         refs = read_info_refs(f)
         (self._refs, self._peeled) = split_peeled_refs(refs)
 
     def allkeys(self):
+        """Get all ref names.
+
+        Returns:
+          All ref names in the info/refs file
+        """
         return self._refs.keys()
 
     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)
 
     def get_packed_refs(self):
+        """Get packed refs (always empty for InfoRefsContainer).
+
+        Returns:
+          Empty dictionary
+        """
         return {}
 
     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:
             return self._peeled[name]
         except KeyError:
@@ -656,6 +819,7 @@ class DiskRefsContainer(RefsContainer):
         worktree_path: Optional[Union[str, bytes, os.PathLike]] = None,
         logger=None,
     ) -> None:
+        """Initialize DiskRefsContainer."""
         super().__init__(logger=logger)
         # Convert path-like objects to strings, then to bytes for Git compatibility
         self.path = os.fsencode(os.fspath(path))
@@ -667,9 +831,18 @@ class DiskRefsContainer(RefsContainer):
         self._peeled_refs = None
 
     def __repr__(self) -> str:
+        """Return string representation of DiskRefsContainer."""
         return f"{self.__class__.__name__}({self.path!r})"
 
     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()
         path = self.refpath(base)
         for root, unused_dirs, files in os.walk(path):
@@ -689,6 +862,11 @@ class DiskRefsContainer(RefsContainer):
         return subkeys
 
     def allkeys(self):
+        """Get all ref names from disk.
+
+        Returns:
+          Set of all ref names (both loose and packed)
+        """
         allkeys = set()
         if os.path.exists(self.refpath(HEADREF)):
             allkeys.add(HEADREF)
@@ -870,6 +1048,9 @@ class DiskRefsContainer(RefsContainer):
         Args:
           name: Name of the ref to set
           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
         """
         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
             unconditionally.
           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
         Returns: True if the set was successful, False otherwise.
         """
@@ -992,6 +1176,9 @@ class DiskRefsContainer(RefsContainer):
         Args:
           name: The refname to set.
           ref: The new sha the refname will refer to.
+          committer: Optional committer name
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Optional message for reflog
         Returns: True if the add was successful, False otherwise.
         """
@@ -1044,6 +1231,9 @@ class DiskRefsContainer(RefsContainer):
           name: The refname to delete.
           old_ref: The old sha the refname must refer to, or None to
             delete unconditionally.
+          committer: Optional committer name
+          timestamp: Optional timestamp
+          timezone: Optional timezone
           message: Optional message
         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):
+    """Read info/refs file.
+
+    Args:
+      f: File-like object to read from
+
+    Returns:
+      Dictionary mapping ref names to SHA1s
+    """
     ret = {}
     for line in f.readlines():
         (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):
+    """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)
 
 
@@ -1356,6 +1562,15 @@ def _import_remote_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 :(
     from .object_store import peel_sha
 
@@ -1385,6 +1600,12 @@ class locked_ref:
     """
 
     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._refname = refname
         self._file: Optional[_GitFile] = None
@@ -1392,6 +1613,14 @@ class locked_ref:
         self._deleted = False
 
     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)
         try:
             realnames, _ = self._refs_container.follow(self._refname)
@@ -1411,6 +1640,13 @@ class locked_ref:
         exc_value: Optional[BaseException],
         traceback: Optional[types.TracebackType],
     ) -> 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 exc_type is not None or self._deleted:
                 self._file.abort()

+ 28 - 0
dulwich/reftable.py

@@ -225,6 +225,13 @@ class RefUpdate:
     """A reference update operation."""
 
     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.value_type = value_type
         self.value = value
@@ -236,6 +243,14 @@ class RefRecord:
     def __init__(
         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.value_type = value_type
         self.value = value
@@ -323,6 +338,7 @@ class RefBlock:
     """A block containing reference records."""
 
     def __init__(self):
+        """Initialize RefBlock."""
         self.refs = []
 
     def add_ref(
@@ -478,6 +494,13 @@ class ReftableWriter:
         auto_create_head: bool = True,
         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.refs: dict[bytes, tuple[int, bytes]] = {}
         self.refs_order: list[bytes] = []  # Track insertion order for update indices
@@ -662,6 +685,11 @@ class ReftableReader:
     """Reader for reftable files."""
 
     def __init__(self, f: BinaryIO):
+        """Initialize ReftableReader.
+
+        Args:
+          f: Binary file object to read from
+        """
         self.f = f
         self._read_header()
         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>'."""
 
     def __init__(self, identity) -> None:
+        """Initialize InvalidUserIdentity exception.
+
+        Args:
+            identity: The invalid identity string
+        """
         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).
 
     Args:
+      config: Configuration stack to read from
       kind: Optional kind to return identity for,
         usually either "AUTHOR" or "COMMITTER".
 
@@ -331,7 +337,16 @@ def _set_filesystem_hidden(path) -> None:
 
 
 class ParentsProvider:
+    """Provides parents for commits, handling grafts and shallow commits."""
+
     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.grafts = grafts
         self.shallows = set(shallows)
@@ -340,6 +355,15 @@ class ParentsProvider:
         self.commit_graph = store.get_commit_graph()
 
     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:
             return self.grafts[commit_id]
         except KeyError:
@@ -581,7 +605,14 @@ class BaseRepo:
                 return None
 
             class DummyMissingObjectFinder:
+                """Dummy finder that returns no missing objects."""
+
                 def get_remote_has(self) -> None:
+                    """Get remote has (always returns None).
+
+                    Returns:
+                      None
+                    """
                     return None
 
                 def __len__(self) -> int:
@@ -607,6 +638,14 @@ class BaseRepo:
         parents_provider = ParentsProvider(self.object_store, shallows=current_shallow)
 
         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 MissingObjectFinder(
@@ -708,6 +747,11 @@ class BaseRepo:
         return self.object_store[sha]
 
     def parents_provider(self) -> ParentsProvider:
+        """Get a parents provider for this repository.
+
+        Returns:
+          ParentsProvider instance configured with grafts and shallows
+        """
         return ParentsProvider(
             self.object_store,
             grafts=self._graftpoints,
@@ -857,26 +901,25 @@ class BaseRepo:
         Args:
           include: Iterable of SHAs of commits to include along with their
             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
         """
@@ -1085,6 +1128,11 @@ class UnsupportedVersion(Exception):
     """Unsupported repository version."""
 
     def __init__(self, version) -> None:
+        """Initialize UnsupportedVersion exception.
+
+        Args:
+            version: The unsupported repository version
+        """
         self.version = version
 
 
@@ -1092,6 +1140,11 @@ class UnsupportedExtension(Exception):
     """Unsupported repository extension."""
 
     def __init__(self, extension) -> None:
+        """Initialize UnsupportedExtension exception.
+
+        Args:
+            extension: The unsupported repository extension
+        """
         self.extension = extension
 
 
@@ -1459,7 +1512,8 @@ class Repo(BaseRepo):
 
     @replace_me(remove_in="0.26.0")
     def unstage(self, fs_paths: list[str]) -> None:
-        """Unstage specific file in the index
+        """Unstage specific file in the index.
+
         Args:
           fs_paths: a list of files to unstage,
             relative to the repository path.
@@ -1581,6 +1635,15 @@ class Repo(BaseRepo):
 
         # Add gitdir matchers
         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 ./)
             if pattern.startswith("./"):
                 # Can't handle relative patterns without config directory context
@@ -1618,6 +1681,14 @@ class Repo(BaseRepo):
 
         # Add onbranch matcher
         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:
                 # Get the current branch using refs
                 ref_chain, _ = self.refs.follow(b"HEAD")
@@ -1640,6 +1711,11 @@ class Repo(BaseRepo):
         return matchers
 
     def get_worktree_config(self) -> "ConfigFile":
+        """Get the worktree-specific config.
+
+        Returns:
+          ConfigFile object for the worktree config
+        """
         from .config import ConfigFile
 
         path = os.path.join(self.commondir(), "config.worktree")
@@ -1694,6 +1770,7 @@ class Repo(BaseRepo):
             return None
 
     def __repr__(self) -> str:
+        """Return string representation of this repository."""
         return f"<Repo at {self.path!r}>"
 
     def set_description(self, description) -> None:
@@ -1756,6 +1833,9 @@ class Repo(BaseRepo):
         Args:
           path: Path in which to create the repository
           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)
         Returns: `Repo` instance
         """
@@ -1843,6 +1923,10 @@ class Repo(BaseRepo):
 
         Args:
           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)
         Returns: a `Repo` instance
         """
@@ -1868,9 +1952,11 @@ class Repo(BaseRepo):
         self.object_store.close()
 
     def __enter__(self):
+        """Enter context manager."""
         return self
 
     def __exit__(self, exc_type, exc_val, exc_tb):
+        """Exit context manager and close repository."""
         self.close()
 
     def _read_gitattributes(self) -> dict[bytes, dict[bytes, bytes]]:
@@ -2060,9 +2146,19 @@ class MemoryRepo(BaseRepo):
         self._reflog.append(args)
 
     def set_description(self, description) -> None:
+        """Set the description for this repository.
+
+        Args:
+          description: Text to set as description
+        """
         self._description = description
 
     def get_description(self):
+        """Get the description of this repository.
+
+        Returns:
+          Repository description as bytes
+        """
         return self._description
 
     def _determine_file_mode(self):
@@ -2103,6 +2199,7 @@ class MemoryRepo(BaseRepo):
 
         Args:
           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.
         """
         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.
 
         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
           get_tagged: Function that returns a dict of pointed-to sha ->
             tag sha for including tags.
@@ -191,6 +193,17 @@ class DictBackend(Backend):
         self.repos = repos
 
     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)
         try:
             return self.repos[path]
@@ -277,6 +290,11 @@ class PackHandler(Handler):
 
     @classmethod
     def innocuous_capabilities(cls) -> Iterable[bytes]:
+        """Return capabilities that don't affect protocol behavior.
+
+        Returns:
+            List of innocuous capability names
+        """
         return [
             CAPABILITY_INCLUDE_TAG,
             CAPABILITY_THIN_PACK,
@@ -600,6 +618,11 @@ class AckGraphWalkerImpl:
     """Base class for acknowledgment graph walker implementations."""
 
     def __init__(self, graph_walker):
+        """Initialize acknowledgment graph walker.
+
+        Args:
+            graph_walker: Graph walker to wrap
+        """
         raise NotImplementedError
 
     def ack(self, have_ref: ObjectID) -> None:
@@ -668,6 +691,8 @@ class _ProtocolGraphWalker:
 
         Args:
           heads: a dict of refname->SHA1 to advertise
+          depth: Maximum depth for shallow clones
+
         Returns: a list of SHA1s requested by the client
         """
         symrefs = self.get_symrefs()
@@ -909,11 +934,21 @@ class SingleAckGraphWalkerImpl(AckGraphWalkerImpl):
         self._common: list[bytes] = []
 
     def ack(self, have_ref) -> None:
+        """Acknowledge a have reference.
+
+        Args:
+            have_ref: Object ID to acknowledge
+        """
         if not self._common:
             self.walker.send_ack(have_ref)
             self._common.append(have_ref)
 
     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)
         if command in (None, COMMAND_DONE):
             # defer the handling of done
@@ -925,6 +960,15 @@ class SingleAckGraphWalkerImpl(AckGraphWalkerImpl):
     __next__ = next
 
     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:
             self.walker.send_nak()
 
@@ -949,11 +993,21 @@ class MultiAckGraphWalkerImpl(AckGraphWalkerImpl):
     """Graph walker implementation that speaks the multi-ack protocol."""
 
     def __init__(self, walker) -> None:
+        """Initialize multi-ack graph walker.
+
+        Args:
+            walker: Parent ProtocolGraphWalker instance
+        """
         self.walker = walker
         self._found_base = False
         self._common: list[bytes] = []
 
     def ack(self, have_ref) -> None:
+        """Acknowledge a have reference.
+
+        Args:
+            have_ref: Object ID to acknowledge
+        """
         self._common.append(have_ref)
         if not self._found_base:
             self.walker.send_ack(have_ref, b"continue")
@@ -962,6 +1016,11 @@ class MultiAckGraphWalkerImpl(AckGraphWalkerImpl):
         # else we blind ack within next
 
     def next(self):
+        """Get next SHA from graph walker.
+
+        Returns:
+            SHA bytes or None if done
+        """
         while True:
             command, sha = self.walker.read_proto_line(_GRAPH_WALKER_COMMANDS)
             if command is None:
@@ -981,6 +1040,15 @@ class MultiAckGraphWalkerImpl(AckGraphWalkerImpl):
     __next__ = next
 
     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:
             # we are not done, especially when done is required; skip
             # 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."""
 
     def __init__(self, walker) -> None:
+        """Initialize multi-ack-detailed graph walker.
+
+        Args:
+            walker: Parent ProtocolGraphWalker instance
+        """
         self.walker = walker
         self._common: list[bytes] = []
 
     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
         self._common.append(have_ref)
         self.walker.send_ack(have_ref, b"common")
 
     def next(self):
+        """Get next SHA from graph walker.
+
+        Returns:
+            SHA bytes or None if done
+        """
         while True:
             command, sha = self.walker.read_proto_line(_GRAPH_WALKER_COMMANDS)
             if command is None:
@@ -1046,6 +1129,15 @@ class MultiAckDetailedGraphWalkerImpl(AckGraphWalkerImpl):
     __next__ = next
 
     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:
             # we are not done, especially when done is required; skip
             # the pack for this request and especially do not handle
@@ -1075,12 +1167,26 @@ class ReceivePackHandler(PackHandler):
     def __init__(
         self, backend, args, proto, stateless_rpc=False, advertise_refs=False
     ) -> 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)
         self.repo = backend.open_repository(args[0])
         self.advertise_refs = advertise_refs
 
     @classmethod
     def capabilities(cls) -> Iterable[bytes]:
+        """Return supported capabilities.
+
+        Returns:
+            List of capability names
+        """
         return [
             CAPABILITY_REPORT_STATUS,
             CAPABILITY_DELETE_REFS,
@@ -1093,6 +1199,14 @@ class ReceivePackHandler(PackHandler):
     def _apply_pack(
         self, refs: list[tuple[ObjectID, ObjectID, Ref]]
     ) -> 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 = (
             IOError,
             OSError,
@@ -1147,6 +1261,11 @@ class ReceivePackHandler(PackHandler):
             yield (ref, ref_status)
 
     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):
             writer = BufferedPktLineWriter(
                 lambda d: self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, d)
@@ -1174,6 +1293,11 @@ class ReceivePackHandler(PackHandler):
         flush()
 
     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)
         if not hook:
             return
@@ -1185,6 +1309,7 @@ class ReceivePackHandler(PackHandler):
             self.proto.write_sideband(SIDE_BAND_CHANNEL_FATAL, str(err).encode("utf-8"))
 
     def handle(self) -> None:
+        """Handle receive-pack request."""
         if self.advertise_refs or not self.stateless_rpc:
             refs = sorted(self.repo.get_refs().items())
             symrefs = sorted(self.repo.refs.get_symrefs().items())
@@ -1235,11 +1360,23 @@ class ReceivePackHandler(PackHandler):
 
 
 class UploadArchiveHandler(Handler):
+    """Handler for git-upload-archive requests."""
+
     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)
         self.repo = backend.open_repository(args[0])
 
     def handle(self) -> None:
+        """Handle upload-archive request."""
+
         def write(x):
             return self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, x)
 
@@ -1287,11 +1424,21 @@ DEFAULT_HANDLERS = {
 
 
 class TCPGitRequestHandler(socketserver.StreamRequestHandler):
+    """TCP request handler for git protocol."""
+
     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
         socketserver.StreamRequestHandler.__init__(self, *args, **kwargs)
 
     def handle(self) -> None:
+        """Handle TCP git request."""
         proto = ReceivableProtocol(self.connection.recv, self.wfile.write)
         command, args = proto.read_cmd()
         logger.info("Handling %s request, args=%s", command, args)
@@ -1304,13 +1451,32 @@ class TCPGitRequestHandler(socketserver.StreamRequestHandler):
 
 
 class TCPGitServer(socketserver.TCPServer):
+    """TCP server for git protocol."""
+
     allow_reuse_address = True
     serve = socketserver.TCPServer.serve_forever
 
     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)
 
     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)
         if handlers is not None:
             self.handlers.update(handlers)
@@ -1319,10 +1485,25 @@ class TCPGitServer(socketserver.TCPServer):
         socketserver.TCPServer.__init__(self, (listen_addr, port), self._make_handler)
 
     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)
         return True
 
     def handle_error(self, request, client_address) -> None:
+        """Handle request processing errors.
+
+        Args:
+            request: Request socket
+            client_address: Client address tuple
+        """
         logger.exception(
             "Exception happened during processing of request from %s",
             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]:
-    """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:
       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:
+        """Initialize Stash.
+
+        Args:
+          repo: Repository object
+          ref: Stash reference name
+        """
         self._ref = ref
         self._repo = repo
 
@@ -76,6 +82,11 @@ class Stash:
         return os.path.join(self._repo.commondir(), "logs", os.fsdecode(self._ref))
 
     def stashes(self) -> list["Entry"]:
+        """Get list of stash entries.
+
+        Returns:
+          List of stash entries in chronological order
+        """
         try:
             with GitFile(self._reflog_path, "rb") as f:
                 return list(reversed(list(read_reflog(f))))
@@ -330,7 +341,9 @@ class Stash:
         return cid
 
     def __getitem__(self, index: int) -> "Entry":
+        """Get stash entry by index."""
         return list(self.stashes())[index]
 
     def __len__(self) -> int:
+        """Return number of stash entries."""
         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:
+    """Base class for testing object store implementations."""
+
     store: "BaseObjectStore"
 
     assertEqual: Callable[[object, object], None]
@@ -62,17 +64,20 @@ class ObjectStoreTests:
     assertFalse: Callable[[bool], None]
 
     def test_determine_wants_all(self) -> None:
+        """Test determine_wants_all with valid ref."""
         self.assertEqual(
             [b"1" * 40],
             self.store.determine_wants_all({b"refs/heads/foo": b"1" * 40}),
         )
 
     def test_determine_wants_all_zero(self) -> None:
+        """Test determine_wants_all with zero ref."""
         self.assertEqual(
             [], self.store.determine_wants_all({b"refs/heads/foo": b"0" * 40})
         )
 
     def test_determine_wants_all_depth(self) -> None:
+        """Test determine_wants_all with depth parameter."""
         self.store.add_object(testobject)
         refs = {b"refs/heads/foo": testobject.id}
         with patch.object(self.store, "_get_depth", return_value=1) as m:
@@ -90,6 +95,7 @@ class ObjectStoreTests:
             )
 
     def test_get_depth(self) -> None:
+        """Test getting object depth."""
         self.assertEqual(0, self.store._get_depth(testobject.id))
 
         self.store.add_object(testobject)
@@ -108,26 +114,29 @@ class ObjectStoreTests:
         )
 
     def test_iter(self) -> None:
+        """Test iterating over empty store."""
         self.assertEqual([], list(self.store))
 
     def test_get_nonexistant(self) -> None:
+        """Test getting non-existent object raises KeyError."""
         self.assertRaises(KeyError, lambda: self.store[b"a" * 40])
 
     def test_contains_nonexistant(self) -> None:
+        """Test checking for non-existent object."""
         self.assertNotIn(b"a" * 40, self.store)
 
     def test_add_objects_empty(self) -> None:
+        """Test adding empty list of objects."""
         self.store.add_objects([])
 
     def test_add_commit(self) -> None:
+        """Test adding commit objects."""
         # TODO: Argh, no way to construct Git commit objects without
         # access to a serialized form.
         self.store.add_objects([])
 
     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")
 
         self.store.add_object(test_object)
@@ -139,6 +148,7 @@ class ObjectStoreTests:
         self.assertEqual(stored_test_object.id, test_object_id)
 
     def test_add_object(self) -> None:
+        """Test adding a single object to store."""
         self.store.add_object(testobject)
         self.assertEqual({testobject.id}, set(self.store))
         self.assertIn(testobject.id, self.store)
@@ -146,6 +156,7 @@ class ObjectStoreTests:
         self.assertEqual(r, testobject)
 
     def test_add_objects(self) -> None:
+        """Test adding multiple objects to store."""
         data = [(testobject, "mypath")]
         self.store.add_objects(data)
         self.assertEqual({testobject.id}, set(self.store))
@@ -154,6 +165,7 @@ class ObjectStoreTests:
         self.assertEqual(r, testobject)
 
     def test_tree_changes(self) -> None:
+        """Test detecting changes between trees."""
         blob_a1 = make_object(Blob, data=b"a1")
         blob_a2 = make_object(Blob, data=b"a2")
         blob_b = make_object(Blob, data=b"b")
@@ -179,6 +191,7 @@ class ObjectStoreTests:
         )
 
     def test_iter_tree_contents(self) -> None:
+        """Test iterating over tree contents."""
         blob_a = make_object(Blob, data=b"a")
         blob_b = make_object(Blob, data=b"b")
         blob_c = make_object(Blob, data=b"c")
@@ -200,6 +213,7 @@ class ObjectStoreTests:
         self.assertEqual([], list(iter_tree_contents(self.store, 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_b = make_object(Blob, data=b"b")
         blob_c = make_object(Blob, data=b"c")
@@ -230,11 +244,13 @@ class ObjectStoreTests:
         self.assertEqual(expected, list(actual))
 
     def make_tag(self, name, obj):
+        """Helper to create and add a tag object."""
         tag = make_tag(obj, name=name)
         self.store.add_object(tag)
         return tag
 
     def test_peel_sha(self) -> None:
+        """Test peeling SHA to get underlying object."""
         self.store.add_object(testobject)
         tag1 = self.make_tag(b"1", testobject)
         tag2 = self.make_tag(b"2", testobject)
@@ -243,17 +259,20 @@ class ObjectStoreTests:
             self.assertEqual((obj, testobject), peel_sha(self.store, obj.id))
 
     def test_get_raw(self) -> None:
+        """Test getting raw object data."""
         self.store.add_object(testobject)
         self.assertEqual(
             (Blob.type_num, b"yummy data"), self.store.get_raw(testobject.id)
         )
 
     def test_close(self) -> None:
+        """Test closing the object store."""
         # For now, just check that close doesn't barf.
         self.store.add_object(testobject)
         self.store.close()
 
     def test_iter_prefix(self) -> None:
+        """Test iterating objects by prefix."""
         self.store.add_object(testobject)
         self.assertEqual([testobject.id], list(self.store.iter_prefix(testobject.id)))
         self.assertEqual(
@@ -296,20 +315,26 @@ class ObjectStoreTests:
         self.assertEqual(blob1.id, objects[0].id)
 
     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)))
 
 
 class PackBasedObjectStoreTests(ObjectStoreTests):
+    """Tests for pack-based object stores."""
+
     store: PackBasedObjectStore
 
     def tearDown(self) -> None:
+        """Clean up by closing all packs."""
         for pack in self.store.packs:
             pack.close()
 
     def test_empty_packs(self) -> None:
+        """Test that new store has no packs."""
         self.assertEqual([], list(self.store.packs))
 
     def test_pack_loose_objects(self) -> None:
+        """Test packing loose objects into packs."""
         b1 = make_object(Blob, data=b"yummy data")
         self.store.add_object(b1)
         b2 = make_object(Blob, data=b"more yummy data")
@@ -324,6 +349,7 @@ class PackBasedObjectStoreTests(ObjectStoreTests):
         self.assertEqual(0, self.store.pack_loose_objects())
 
     def test_repack(self) -> None:
+        """Test repacking multiple packs into one."""
         b1 = make_object(Blob, data=b"yummy data")
         self.store.add_object(b1)
         b2 = make_object(Blob, data=b"more yummy data")
@@ -341,6 +367,7 @@ class PackBasedObjectStoreTests(ObjectStoreTests):
         self.assertEqual(0, self.store.pack_loose_objects())
 
     def test_repack_existing(self) -> None:
+        """Test repacking with existing objects."""
         b1 = make_object(Blob, data=b"yummy data")
         self.store.add_object(b1)
         b2 = make_object(Blob, data=b"more yummy data")
@@ -401,16 +428,21 @@ class PackBasedObjectStoreTests(ObjectStoreTests):
 
 
 class FindShallowTests(TestCase):
+    """Tests for finding shallow commits."""
+
     def setUp(self):
+        """Set up test fixture."""
         super().setUp()
         self._store = MemoryObjectStore()
 
     def make_commit(self, **attrs):
+        """Helper to create and store a commit."""
         commit = make_commit(**attrs)
         self._store.add_object(commit)
         return commit
 
     def make_linear_commits(self, n, message=b""):
+        """Create a linear chain of commits."""
         commits = []
         parents = []
         for _ in range(n):
@@ -419,9 +451,11 @@ class FindShallowTests(TestCase):
         return commits
 
     def assertSameElements(self, expected, actual):
+        """Assert that two sequences contain the same elements."""
         self.assertEqual(set(expected), set(actual))
 
     def test_linear(self):
+        """Test finding shallow commits in a linear history."""
         c1, c2, c3 = self.make_linear_commits(3)
 
         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):
+        """Test finding shallow commits with multiple independent branches."""
         a = self.make_linear_commits(2, message=b"a")
         b = self.make_linear_commits(2, message=b"b")
         c = self.make_linear_commits(2, message=b"c")
@@ -450,6 +485,7 @@ class FindShallowTests(TestCase):
         )
 
     def test_multiple_overlapping(self):
+        """Test finding shallow commits with overlapping branches."""
         # Create the following commit tree:
         # 1--2
         #  \
@@ -465,6 +501,7 @@ class FindShallowTests(TestCase):
         )
 
     def test_merge(self):
+        """Test finding shallow commits with merge commits."""
         c1 = self.make_commit()
         c2 = self.make_commit()
         c3 = self.make_commit(parents=[c1.id, c2.id])
@@ -475,6 +512,7 @@ class FindShallowTests(TestCase):
         )
 
     def test_tag(self):
+        """Test finding shallow commits with tags."""
         c1, c2 = self.make_linear_commits(2)
         tag = make_tag(c2, name=b"tag")
         self._store.add_object(tag)

+ 1 - 0
dulwich/tests/utils.py

@@ -87,6 +87,7 @@ def make_object(cls, **attrs):
     __slots__.
 
     Args:
+      cls: The class to create an instance of
       attrs: dict of attributes to set on the new object.
     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."""
 
     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._store = walker.store
         self._get_parents = walker.get_parents
@@ -141,6 +147,7 @@ class WalkEntry:
         return self._changes[path_prefix]
 
     def __repr__(self) -> str:
+        """Return string representation of WalkEntry."""
         return f"<WalkEntry commit={self.commit.id.decode('ascii')}, changes={self.changes()!r}>"
 
 
@@ -435,6 +442,7 @@ class Walker:
         return results
 
     def __iter__(self) -> Iterator[WalkEntry]:
+        """Iterate over walk entries."""
         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]]:
+    """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:
         now = time.time()
     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:
+    """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
     # Python 2.6.5 standard library, following modifications:
     #  - Made a global rather than an instance method.
@@ -164,6 +180,16 @@ def _url_to_path(url: str) -> str:
 def get_text_file(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> 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()
     path = _url_to_path(mat.group())
     logger.info("Sending plain text file %s", path)
@@ -173,6 +199,16 @@ def get_text_file(
 def get_loose_object(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> 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")
     logger.info("Sending loose object %s", sha)
     object_store = get_repo(backend, mat).object_store
@@ -192,6 +228,16 @@ def get_loose_object(
 def get_pack_file(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> 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()
     path = _url_to_path(mat.group())
     logger.info("Sending pack file %s", path)
@@ -205,6 +251,16 @@ def get_pack_file(
 def get_idx_file(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> 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()
     path = _url_to_path(mat.group())
     logger.info("Sending pack file %s", path)
@@ -218,6 +274,16 @@ def get_idx_file(
 def get_info_refs(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> 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"])
     service = params.get("service", [None])[0]
     try:
@@ -255,6 +321,16 @@ def get_info_refs(
 def get_info_packs(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> 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.respond(HTTP_OK, "text/plain")
     logger.info("Emulating dumb info/packs")
@@ -275,10 +351,23 @@ class ChunkReader:
     """Reader for chunked transfer encoding streams."""
 
     def __init__(self, f: BinaryIO) -> None:
+        """Initialize ChunkReader.
+
+        Args:
+            f: Binary file-like object to read from
+        """
         self._iter = _chunk_iter(f)
         self._buffer: list[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:
             try:
                 self._buffer.append(next(self._iter))
@@ -303,6 +392,14 @@ class _LengthLimitedFile:
         self._bytes_avail = max_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:
             return b""
         if size == -1 or size > self._bytes_avail:
@@ -316,6 +413,16 @@ class _LengthLimitedFile:
 def handle_service_request(
     req: "HTTPGitRequest", backend: "Backend", mat: re.Match[str]
 ) -> 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("/")
     logger.info("Handling service request for %s", service)
     handler_cls = req.handlers.get(service.encode("ascii"), None)
@@ -350,6 +457,14 @@ class HTTPGitRequest:
     def __init__(
         self, environ, start_response, dumb: bool = False, handlers=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.dumb = dumb
         self.handlers = handlers
@@ -443,6 +558,14 @@ class HTTPGitApplication:
     def __init__(
         self, backend, dumb: bool = False, handlers=None, fallback_app=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.dumb = dumb
         self.handlers = dict(DEFAULT_HANDLERS)
@@ -451,6 +574,7 @@ class HTTPGitApplication:
             self.handlers.update(handlers)
 
     def __call__(self, environ, start_response):
+        """Handle WSGI request."""
         path = environ["PATH_INFO"]
         method = environ["REQUEST_METHOD"]
         req = HTTPGitRequest(
@@ -476,14 +600,14 @@ class HTTPGitApplication:
 
 
 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:
+        """Initialize GunzipFilter."""
         self.app = application
 
     def __call__(self, environ, start_response):
+        """Handle WSGI request."""
         import gzip
 
         if environ.get("HTTP_CONTENT_ENCODING", "") == "gzip":
@@ -498,14 +622,14 @@ class GunzipFilter:
 
 
 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:
+        """Initialize LimitedInputFilter."""
         self.app = application
 
     def __call__(self, environ, start_response):
+        """Handle WSGI request."""
         # 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.
         # TODO: git may used HTTP/1.1 chunked encoding instead of specifying
@@ -519,9 +643,7 @@ class LimitedInputFilter:
 
 
 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)
     wrapped_app = LimitedInputFilter(GunzipFilter(app))
     return wrapped_app
@@ -531,15 +653,31 @@ class ServerHandlerLogger(ServerHandler):
     """ServerHandler that uses dulwich's logger for logging exceptions."""
 
     def log_exception(self, exc_info) -> None:
+        """Log an exception using dulwich's logger.
+
+        Args:
+          exc_info: Exception information tuple
+        """
         logger.exception(
             "Exception happened during processing of request",
             exc_info=exc_info,
         )
 
     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)
 
     def log_error(self, *args) -> None:
+        """Log an error using dulwich's logger.
+
+        Args:
+          *args: Error message components
+        """
         logger.error(*args)
 
 
@@ -547,15 +685,31 @@ class WSGIRequestHandlerLogger(WSGIRequestHandler):
     """WSGIRequestHandler that uses dulwich's logger for logging exceptions."""
 
     def log_exception(self, exc_info) -> None:
+        """Log an exception using dulwich's logger.
+
+        Args:
+          exc_info: Exception information tuple
+        """
         logger.exception(
             "Exception happened during processing of request",
             exc_info=exc_info,
         )
 
     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)
 
     def log_error(self, *args) -> None:
+        """Log an error using dulwich's logger.
+
+        Args:
+          *args: Error message components
+        """
         logger.error(*args)
 
     def handle(self) -> None:

+ 16 - 1
dulwich/worktree.py

@@ -72,6 +72,18 @@ class WorkTreeInfo:
         prunable: bool = False,
         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.head = head
         self.branch = branch
@@ -82,9 +94,11 @@ class WorkTreeInfo:
         self.lock_reason = lock_reason
 
     def __repr__(self) -> str:
+        """Return string representation of WorkTreeInfo."""
         return f"WorkTreeInfo(path={self.path!r}, branch={self.branch!r}, detached={self.detached})"
 
     def __eq__(self, other: object) -> bool:
+        """Check equality with another WorkTreeInfo."""
         if not isinstance(other, WorkTreeInfo):
             return NotImplemented
         return (
@@ -311,7 +325,8 @@ class WorkTree:
         index.write()
 
     def unstage(self, fs_paths: list[str]) -> None:
-        """Unstage specific file in the index
+        """Unstage specific file in the index.
+
         Args:
           fs_paths: a list of files to unstage,
             relative to the repository path.

+ 8 - 10
pyproject.toml

@@ -98,22 +98,20 @@ ignore = [
     "ANN204",
     "ANN205",
     "ANN206",
-    "D100",
-    "D101",
-    "D102",
-    "D103",
-    "D104",
-    "D105",
-    "D107",
-    "D204",
-    "D205",
-    "D417",
     "E501",  # line too long
 ]
 
 [tool.ruff.lint.pydocstyle]
 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]
 skip = "cp314-*"
 environment = {PATH="$HOME/.cargo/bin:$PATH"}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio