| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745 |
- # server.py -- Implementation of the server side git protocols
- # Copyright (C) 2008 John Carr <john.carr@unrouted.co.uk>
- # Copyright(C) 2011-2012 Jelmer Vernooij <jelmer@jelmer.uk>
- #
- # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
- # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
- # General Public License as published by the Free Software Foundation; version 2.0
- # or (at your option) any later version. You can redistribute it and/or
- # modify it under the terms of either of these two licenses.
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- #
- # You should have received a copy of the licenses; if not, see
- # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
- # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
- # License, Version 2.0.
- #
- """Git smart network protocol server implementation.
- For more detailed implementation on the network protocol, see the
- Documentation/technical directory in the cgit distribution, and in particular:
- * Documentation/technical/protocol-capabilities.txt
- * Documentation/technical/pack-protocol.txt
- Currently supported capabilities:
- * include-tag
- * thin-pack
- * multi_ack_detailed
- * multi_ack
- * side-band-64k
- * ofs-delta
- * no-progress
- * report-status
- * delete-refs
- * shallow
- * symref
- """
- import collections
- import os
- import socket
- import socketserver
- import sys
- import time
- import zlib
- from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
- from collections.abc import Set as AbstractSet
- from functools import partial
- from typing import IO, TYPE_CHECKING, Optional
- from typing import Protocol as TypingProtocol
- if sys.version_info >= (3, 12):
- from collections.abc import Buffer
- else:
- Buffer = bytes | bytearray | memoryview
- if TYPE_CHECKING:
- from .object_store import BaseObjectStore
- from .repo import BaseRepo
- from dulwich import log_utils
- from .archive import tar_stream
- from .errors import (
- ApplyDeltaError,
- ChecksumMismatch,
- GitProtocolError,
- HookError,
- NotGitRepository,
- ObjectFormatException,
- UnexpectedCommandError,
- )
- from .object_store import MissingObjectFinder, PackBasedObjectStore, find_shallow
- from .objects import Commit, ObjectID, Tree, valid_hexsha
- from .pack import ObjectContainer, write_pack_from_container
- from .protocol import (
- CAPABILITIES_REF,
- CAPABILITY_AGENT,
- CAPABILITY_DELETE_REFS,
- CAPABILITY_INCLUDE_TAG,
- CAPABILITY_MULTI_ACK,
- CAPABILITY_MULTI_ACK_DETAILED,
- CAPABILITY_NO_DONE,
- CAPABILITY_NO_PROGRESS,
- CAPABILITY_OFS_DELTA,
- CAPABILITY_QUIET,
- CAPABILITY_REPORT_STATUS,
- CAPABILITY_SHALLOW,
- CAPABILITY_SIDE_BAND_64K,
- CAPABILITY_THIN_PACK,
- COMMAND_DEEPEN,
- COMMAND_DONE,
- COMMAND_HAVE,
- COMMAND_SHALLOW,
- COMMAND_UNSHALLOW,
- COMMAND_WANT,
- MULTI_ACK,
- MULTI_ACK_DETAILED,
- NAK_LINE,
- SIDE_BAND_CHANNEL_DATA,
- SIDE_BAND_CHANNEL_FATAL,
- SIDE_BAND_CHANNEL_PROGRESS,
- SINGLE_ACK,
- TCP_GIT_PORT,
- ZERO_SHA,
- BufferedPktLineWriter,
- Protocol,
- ReceivableProtocol,
- ack_type,
- capability_agent,
- extract_capabilities,
- extract_want_line_capabilities,
- format_ack_line,
- format_ref_line,
- format_shallow_line,
- format_unshallow_line,
- symref_capabilities,
- )
- from .refs import PEELED_TAG_SUFFIX, Ref, RefsContainer, write_info_refs
- from .repo import Repo
- logger = log_utils.getLogger(__name__)
- class Backend:
- """A backend for the Git smart server implementation."""
- def open_repository(self, path: str) -> "BackendRepo":
- """Open the repository at a path.
- Args:
- path: Path to the repository
- Raises:
- NotGitRepository: no git repository was found at path
- Returns: Instance of BackendRepo
- """
- raise NotImplementedError(self.open_repository)
- class BackendRepo(TypingProtocol):
- """Repository abstraction used by the Git server.
- The methods required here are a subset of those provided by
- dulwich.repo.Repo.
- """
- object_store: PackBasedObjectStore
- refs: RefsContainer
- def get_refs(self) -> dict[bytes, bytes]:
- """Get all the refs in the repository.
- Returns: dict of name -> sha
- """
- raise NotImplementedError
- def get_peeled(self, name: bytes) -> bytes | None:
- """Return the cached peeled value of a ref, if available.
- Args:
- name: Name of the ref to peel
- Returns: The peeled value of the ref. If the ref is known not point to
- a tag, this will be the SHA the ref refers to. If no cached
- information about a tag is available, this method may return None,
- but it should attempt to peel the tag if possible.
- """
- return None
- def find_missing_objects(
- self,
- determine_wants: Callable[[Mapping[bytes, bytes], int | None], list[bytes]],
- graph_walker: "_ProtocolGraphWalker",
- progress: Callable[[bytes], None] | None,
- *,
- get_tagged: Callable[[], dict[bytes, bytes]] | None = None,
- depth: int | None = None,
- ) -> Optional["MissingObjectFinder"]:
- """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.
- depth: Maximum depth of commits to fetch for shallow clones
- """
- raise NotImplementedError
- class DictBackend(Backend):
- """Trivial backend that looks up Git repositories in a dictionary."""
- def __init__(
- self, repos: dict[bytes, "BackendRepo"] | dict[str, "BackendRepo"]
- ) -> None:
- """Initialize a DictBackend.
- Args:
- repos: Dictionary mapping repository paths to BackendRepo instances
- """
- 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)
- # Handle both str and bytes keys for backward compatibility
- if path in self.repos:
- return self.repos[path] # type: ignore
- # Try converting between str and bytes
- if isinstance(path, bytes):
- try:
- alt_path = path.decode("utf-8")
- if alt_path in self.repos:
- return self.repos[alt_path]
- except UnicodeDecodeError:
- pass
- else:
- alt_path_bytes = path.encode("utf-8")
- if alt_path_bytes in self.repos:
- return self.repos[alt_path_bytes] # type: ignore
- raise NotGitRepository(
- "No git repository was found at {path}".format(**dict(path=path))
- )
- class FileSystemBackend(Backend):
- """Simple backend looking up Git repositories in the local file system."""
- def __init__(self, root: str = os.sep) -> None:
- """Initialize a FileSystemBackend.
- Args:
- root: Root directory to serve repositories from
- """
- super().__init__()
- self.root = (os.path.abspath(root) + os.sep).replace(os.sep * 2, os.sep)
- def open_repository(self, path: str) -> BackendRepo:
- """Open a repository from the filesystem.
- Args:
- path: Path to the repository relative to the root
- Returns: Repo instance
- Raises:
- NotGitRepository: If path is outside the root or not a git repository
- """
- logger.debug("opening repository at %s", path)
- # Ensure path is a string to avoid TypeError when joining with self.root
- path = os.fspath(path)
- if isinstance(path, bytes):
- path = os.fsdecode(path)
- abspath = os.path.abspath(os.path.join(self.root, path)) + os.sep
- normcase_abspath = os.path.normcase(abspath)
- normcase_root = os.path.normcase(self.root)
- if not normcase_abspath.startswith(normcase_root):
- raise NotGitRepository(f"Path {path!r} not inside root {self.root!r}")
- return Repo(abspath) # type: ignore[return-value]
- class Handler:
- """Smart protocol command handler base class."""
- def __init__(
- self, backend: Backend, proto: Protocol, stateless_rpc: bool = False
- ) -> None:
- """Initialize a Handler.
- Args:
- backend: Backend instance for repository access
- proto: Protocol instance for communication
- stateless_rpc: Whether this is a stateless RPC session
- """
- self.backend = backend
- self.proto = proto
- self.stateless_rpc = stateless_rpc
- def handle(self) -> None:
- """Handle a request."""
- raise NotImplementedError(self.handle)
- class PackHandler(Handler):
- """Protocol handler for packs."""
- def __init__(
- self, backend: Backend, proto: Protocol, stateless_rpc: bool = False
- ) -> None:
- """Initialize a PackHandler.
- Args:
- backend: Backend instance for repository access
- proto: Protocol instance for communication
- stateless_rpc: Whether this is a stateless RPC session
- """
- super().__init__(backend, proto, stateless_rpc)
- self._client_capabilities: set[bytes] | None = None
- # Flags needed for the no-done capability
- self._done_received = False
- self.advertise_refs = False
- @classmethod
- def capabilities(cls) -> Iterable[bytes]:
- """Return a list of capabilities supported by this handler."""
- raise NotImplementedError(cls.capabilities)
- @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,
- CAPABILITY_NO_PROGRESS,
- CAPABILITY_OFS_DELTA,
- capability_agent(),
- ]
- @classmethod
- def required_capabilities(cls) -> Iterable[bytes]:
- """Return a list of capabilities that we require the client to have."""
- return []
- def set_client_capabilities(self, caps: Iterable[bytes]) -> None:
- """Set the client capabilities and validate them.
- Args:
- caps: List of capabilities requested by the client
- Raises:
- GitProtocolError: If client requests unsupported capability or lacks required ones
- """
- allowable_caps = set(self.innocuous_capabilities())
- allowable_caps.update(self.capabilities())
- for cap in caps:
- if cap.startswith(CAPABILITY_AGENT + b"="):
- continue
- if cap not in allowable_caps:
- raise GitProtocolError(
- f"Client asked for capability {cap!r} that was not advertised."
- )
- for cap in self.required_capabilities():
- if cap not in caps:
- raise GitProtocolError(
- f"Client does not support required capability {cap!r}."
- )
- self._client_capabilities = set(caps)
- logger.info("Client capabilities: %s", caps)
- def has_capability(self, cap: bytes) -> bool:
- """Check if the client supports a specific capability.
- Args:
- cap: Capability name to check
- Returns: True if the client supports the capability
- Raises:
- GitProtocolError: If called before client capabilities are set
- """
- if self._client_capabilities is None:
- raise GitProtocolError(
- f"Server attempted to access capability {cap!r} before asking client"
- )
- return cap in self._client_capabilities
- def notify_done(self) -> None:
- """Notify that the 'done' command has been received from the client."""
- self._done_received = True
- class UploadPackHandler(PackHandler):
- """Protocol handler for uploading a pack to the client."""
- def __init__(
- self,
- backend: Backend,
- args: Sequence[str],
- proto: Protocol,
- stateless_rpc: bool = False,
- advertise_refs: bool = False,
- ) -> None:
- """Initialize an UploadPackHandler.
- Args:
- backend: Backend instance for repository access
- args: Command arguments (first arg is repository path)
- proto: Protocol instance for communication
- stateless_rpc: Whether this is a stateless RPC session
- advertise_refs: Whether to advertise refs
- """
- super().__init__(backend, proto, stateless_rpc=stateless_rpc)
- self.repo = backend.open_repository(args[0])
- self._graph_walker = None
- self.advertise_refs = advertise_refs
- # A state variable for denoting that the have list is still
- # being processed, and the client is not accepting any other
- # data (such as side-band, see the progress method here).
- self._processing_have_lines = False
- @classmethod
- def capabilities(cls) -> list[bytes]:
- """Return the list of capabilities supported by upload-pack."""
- return [
- CAPABILITY_MULTI_ACK_DETAILED,
- CAPABILITY_MULTI_ACK,
- CAPABILITY_SIDE_BAND_64K,
- CAPABILITY_THIN_PACK,
- CAPABILITY_OFS_DELTA,
- CAPABILITY_NO_PROGRESS,
- CAPABILITY_INCLUDE_TAG,
- CAPABILITY_SHALLOW,
- CAPABILITY_NO_DONE,
- ]
- @classmethod
- def required_capabilities(cls) -> tuple[bytes, ...]:
- """Return the list of capabilities required for upload-pack."""
- return (
- CAPABILITY_SIDE_BAND_64K,
- CAPABILITY_THIN_PACK,
- CAPABILITY_OFS_DELTA,
- )
- def progress(self, message: bytes) -> None:
- """Send a progress message to the client.
- Args:
- message: Progress message to send
- """
- def _start_pack_send_phase(self) -> None:
- """Start the pack sending phase, setting up sideband if supported."""
- if self.has_capability(CAPABILITY_SIDE_BAND_64K):
- # The provided haves are processed, and it is safe to send side-
- # band data now.
- if not self.has_capability(CAPABILITY_NO_PROGRESS):
- self.progress = partial( # type: ignore
- self.proto.write_sideband, SIDE_BAND_CHANNEL_PROGRESS
- )
- self.write_pack_data: Callable[[bytes], None] = partial(
- self.proto.write_sideband, SIDE_BAND_CHANNEL_DATA
- )
- else:
- # proto.write returns Optional[int], but we need to treat it as returning None
- # for compatibility with write_pack_from_container
- def write_data(data: bytes) -> None:
- self.proto.write(data)
- self.write_pack_data = write_data
- def get_tagged(
- self,
- refs: Mapping[bytes, bytes] | None = None,
- repo: BackendRepo | None = None,
- ) -> dict[ObjectID, ObjectID]:
- """Get a dict of peeled values of tags to their original tag shas.
- Args:
- refs: dict of refname -> sha of possible tags; defaults to all
- of the backend's refs.
- repo: optional Repo instance for getting peeled refs; defaults
- to the backend's repo, if available
- Returns: dict of peeled_sha -> tag_sha, where tag_sha is the sha of a
- tag whose peeled value is peeled_sha.
- """
- if not self.has_capability(CAPABILITY_INCLUDE_TAG):
- return {}
- if refs is None:
- refs = self.repo.get_refs()
- if repo is None:
- repo = getattr(self.repo, "repo", None)
- if repo is None:
- # Bail if we don't have a Repo available; this is ok since
- # clients must be able to handle if the server doesn't include
- # all relevant tags.
- # TODO: fix behavior when missing
- return {}
- # TODO(jelmer): Integrate this with the refs logic in
- # Repo.find_missing_objects
- tagged = {}
- for name, sha in refs.items():
- peeled_sha = repo.get_peeled(name)
- if peeled_sha is not None and peeled_sha != sha:
- tagged[peeled_sha] = sha
- return tagged
- def handle(self) -> None:
- """Handle an upload-pack request.
- This method processes the client's wants and haves, determines which
- objects to send, and writes the pack data to the client.
- """
- # Note the fact that client is only processing responses related
- # to the have lines it sent, and any other data (including side-
- # band) will be be considered a fatal error.
- self._processing_have_lines = True
- graph_walker = _ProtocolGraphWalker(
- self,
- self.repo.object_store,
- self.repo.get_peeled,
- self.repo.refs.get_symrefs,
- )
- wants = []
- def wants_wrapper(
- refs: Mapping[bytes, bytes], depth: int | None = None
- ) -> list[bytes]:
- wants.extend(graph_walker.determine_wants(refs, depth))
- return wants
- missing_objects = self.repo.find_missing_objects(
- wants_wrapper,
- graph_walker,
- self.progress,
- get_tagged=self.get_tagged,
- )
- # Did the process short-circuit (e.g. in a stateless RPC call)? Note
- # that the client still expects a 0-object pack in most cases.
- # Also, if it also happens that the object_iter is instantiated
- # with a graph walker with an implementation that talks over the
- # wire (which is this instance of this class) this will actually
- # iterate through everything and write things out to the wire.
- if len(wants) == 0:
- return
- # Handle shallow clone case where missing_objects can be None
- if missing_objects is None:
- return
- object_ids = list(missing_objects)
- if not graph_walker.handle_done(
- not self.has_capability(CAPABILITY_NO_DONE), self._done_received
- ):
- return
- self._start_pack_send_phase()
- self.progress((f"counting objects: {len(object_ids)}, done.\n").encode("ascii"))
- write_pack_from_container(
- self.write_pack_data,
- self.repo.object_store,
- object_ids,
- )
- # we are done
- self.proto.write_pkt_line(None)
- def _split_proto_line(
- line: bytes | None, allowed: Iterable[bytes | None] | None
- ) -> tuple[bytes | None, bytes | int | None]:
- """Split a line read from the wire.
- Args:
- line: The line read from the wire.
- allowed: An iterable of command names that should be allowed.
- Command names not listed below as possible return values will be
- ignored. If None, any commands from the possible return values are
- allowed.
- Returns: a tuple having one of the following forms:
- ('want', obj_id)
- ('have', obj_id)
- ('done', None)
- (None, None) (for a flush-pkt)
- Raises:
- UnexpectedCommandError: if the line cannot be parsed into one of the
- allowed return values.
- """
- if not line:
- fields: list[bytes | None] = [None]
- else:
- fields = list(line.rstrip(b"\n").split(b" ", 1))
- command = fields[0]
- if allowed is not None and command not in allowed:
- raise UnexpectedCommandError(command.decode("utf-8") if command else None)
- if len(fields) == 1 and command in (COMMAND_DONE, None):
- return (command, None)
- elif len(fields) == 2:
- if command in (
- COMMAND_WANT,
- COMMAND_HAVE,
- COMMAND_SHALLOW,
- COMMAND_UNSHALLOW,
- ):
- assert fields[1] is not None
- if not valid_hexsha(fields[1]):
- raise GitProtocolError("Invalid sha")
- return (command, fields[1])
- elif command == COMMAND_DEEPEN:
- assert fields[1] is not None
- return command, int(fields[1])
- raise GitProtocolError(f"Received invalid line from client: {line!r}")
- def _want_satisfied(
- store: ObjectContainer, haves: set[bytes], want: bytes, earliest: int
- ) -> bool:
- """Check if a specific want is satisfied by a set of haves.
- Args:
- store: Object store to retrieve objects from
- haves: Set of commit IDs the client has
- want: Commit ID the client wants
- earliest: Earliest commit time to consider
- Returns: True if the want is satisfied by the haves
- """
- o = store[want]
- pending = collections.deque([o])
- known = {want}
- while pending:
- commit = pending.popleft()
- if commit.id in haves:
- return True
- if not isinstance(commit, Commit):
- # non-commit wants are assumed to be satisfied
- continue
- for parent in commit.parents:
- if parent in known:
- continue
- known.add(parent)
- parent_obj = store[parent]
- assert isinstance(parent_obj, Commit)
- # TODO: handle parents with later commit times than children
- if parent_obj.commit_time >= earliest:
- pending.append(parent_obj)
- return False
- def _all_wants_satisfied(
- store: ObjectContainer, haves: AbstractSet[bytes], wants: set[bytes]
- ) -> bool:
- """Check whether all the current wants are satisfied by a set of haves.
- Args:
- store: Object store to retrieve objects from
- haves: A set of commits we know the client has.
- wants: A set of commits the client wants
- Note: Wants are specified with set_wants rather than passed in since
- in the current interface they are determined outside this class.
- """
- haves = set(haves)
- if haves:
- have_objs = [store[h] for h in haves]
- earliest = min([h.commit_time for h in have_objs if isinstance(h, Commit)])
- else:
- earliest = 0
- for want in wants:
- if not _want_satisfied(store, haves, want, earliest):
- return False
- return True
- class AckGraphWalkerImpl:
- """Base class for acknowledgment graph walker implementations."""
- def __init__(self, graph_walker: "_ProtocolGraphWalker") -> None:
- """Initialize acknowledgment graph walker.
- Args:
- graph_walker: Graph walker to wrap
- """
- raise NotImplementedError
- def ack(self, have_ref: ObjectID) -> None:
- """Acknowledge a have reference.
- Args:
- have_ref: Object ID to acknowledge
- """
- raise NotImplementedError
- def handle_done(self, done_required: bool, done_received: bool) -> bool:
- """Handle 'done' packet from client."""
- raise NotImplementedError
- class _ProtocolGraphWalker:
- """A graph walker that knows the git protocol.
- As a graph walker, this class implements ack(), next(), and reset(). It
- also contains some base methods for interacting with the wire and walking
- the commit tree.
- The work of determining which acks to send is passed on to the
- implementation instance stored in _impl. The reason for this is that we do
- not know at object creation time what ack level the protocol requires. A
- call to set_ack_type() is required to set up the implementation, before
- any calls to next() or ack() are made.
- """
- def __init__(
- self,
- handler: PackHandler,
- object_store: ObjectContainer,
- get_peeled: Callable[[bytes], bytes | None],
- get_symrefs: Callable[[], dict[bytes, bytes]],
- ) -> None:
- """Initialize a ProtocolGraphWalker.
- Args:
- handler: Protocol handler instance
- object_store: Object store for retrieving objects
- get_peeled: Function to get peeled refs
- get_symrefs: Function to get symbolic refs
- """
- self.handler = handler
- self.store: ObjectContainer = object_store
- self.get_peeled = get_peeled
- self.get_symrefs = get_symrefs
- self.proto = handler.proto
- self.stateless_rpc = handler.stateless_rpc
- self.advertise_refs = handler.advertise_refs
- self._wants: list[bytes] = []
- self.shallow: set[bytes] = set()
- self.client_shallow: set[bytes] = set()
- self.unshallow: set[bytes] = set()
- self._cached = False
- self._cache: list[bytes] = []
- self._cache_index = 0
- self._impl: AckGraphWalkerImpl | None = None
- def determine_wants(
- self, heads: Mapping[bytes, bytes], depth: int | None = None
- ) -> list[bytes]:
- """Determine the wants for a set of heads.
- The given heads are advertised to the client, who then specifies which
- refs they want using 'want' lines. This portion of the protocol is the
- same regardless of ack type, and in fact is used to set the ack type of
- the ProtocolGraphWalker.
- If the client has the 'shallow' capability, this method also reads and
- responds to the 'shallow' and 'deepen' lines from the client. These are
- not part of the wants per se, but they set up necessary state for
- walking the graph. Additionally, later code depends on this method
- consuming everything up to the first 'have' line.
- 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()
- values = set(heads.values())
- if self.advertise_refs or not self.stateless_rpc:
- for i, (ref, sha) in enumerate(sorted(heads.items())):
- try:
- peeled_sha = self.get_peeled(ref)
- except KeyError:
- # Skip refs that are inaccessible
- # TODO(jelmer): Integrate with Repo.find_missing_objects refs
- # logic.
- continue
- if i == 0:
- logger.info("Sending capabilities: %s", self.handler.capabilities())
- line = format_ref_line(
- ref,
- sha,
- list(self.handler.capabilities())
- + symref_capabilities(symrefs.items()),
- )
- else:
- line = format_ref_line(ref, sha)
- self.proto.write_pkt_line(line)
- if peeled_sha is not None and peeled_sha != sha:
- self.proto.write_pkt_line(
- format_ref_line(ref + PEELED_TAG_SUFFIX, peeled_sha)
- )
- # i'm done..
- self.proto.write_pkt_line(None)
- if self.advertise_refs:
- return []
- # Now client will sending want want want commands
- want = self.proto.read_pkt_line()
- if not want:
- return []
- line, caps = extract_want_line_capabilities(want)
- self.handler.set_client_capabilities(caps)
- self.set_ack_type(ack_type(caps))
- allowed = (COMMAND_WANT, COMMAND_SHALLOW, COMMAND_DEEPEN, None)
- command, sha_result = _split_proto_line(line, allowed)
- want_revs = []
- while command == COMMAND_WANT:
- assert isinstance(sha_result, bytes)
- if sha_result not in values:
- raise GitProtocolError(f"Client wants invalid object {sha_result!r}")
- want_revs.append(sha_result)
- command, sha_result = self.read_proto_line(allowed)
- self.set_wants(want_revs)
- if command in (COMMAND_SHALLOW, COMMAND_DEEPEN):
- assert sha_result is not None
- self.unread_proto_line(command, sha_result)
- self._handle_shallow_request(want_revs)
- if self.stateless_rpc and self.proto.eof():
- # The client may close the socket at this point, expecting a
- # flush-pkt from the server. We might be ready to send a packfile
- # at this point, so we need to explicitly short-circuit in this
- # case.
- return []
- return want_revs
- def unread_proto_line(self, command: bytes, value: bytes | int) -> None:
- """Push a command back to be read again.
- Args:
- command: Command name
- value: Command value
- """
- if isinstance(value, int):
- value = str(value).encode("ascii")
- self.proto.unread_pkt_line(command + b" " + value)
- def nak(self) -> None:
- """Send a NAK response."""
- def ack(self, have_ref: bytes) -> None:
- """Acknowledge a have reference.
- Args:
- have_ref: SHA to acknowledge (40 bytes hex)
- Raises:
- ValueError: If have_ref is not 40 bytes
- """
- if len(have_ref) != 40:
- raise ValueError(f"invalid sha {have_ref!r}")
- assert self._impl is not None
- return self._impl.ack(have_ref)
- def reset(self) -> None:
- """Reset the graph walker cache."""
- self._cached = True
- self._cache_index = 0
- def next(self) -> bytes | None:
- """Get the next SHA from the graph walker.
- Returns: Next SHA or None if done
- """
- if not self._cached:
- if not self._impl and self.stateless_rpc:
- return None
- assert self._impl is not None
- return next(self._impl) # type: ignore[call-overload, no-any-return]
- self._cache_index += 1
- if self._cache_index > len(self._cache):
- return None
- return self._cache[self._cache_index]
- __next__ = next
- def read_proto_line(
- self, allowed: Iterable[bytes | None] | None
- ) -> tuple[bytes | None, bytes | int | None]:
- """Read a line from the wire.
- Args:
- allowed: An iterable of command names that should be allowed.
- Returns: A tuple of (command, value); see _split_proto_line.
- Raises:
- UnexpectedCommandError: If an error occurred reading the line.
- """
- return _split_proto_line(self.proto.read_pkt_line(), allowed)
- def _handle_shallow_request(self, wants: Sequence[bytes]) -> None:
- """Handle shallow clone requests from the client.
- Args:
- wants: List of wanted object SHAs
- """
- while True:
- command, val = self.read_proto_line((COMMAND_DEEPEN, COMMAND_SHALLOW))
- if command == COMMAND_DEEPEN:
- assert isinstance(val, int)
- depth = val
- break
- assert isinstance(val, bytes)
- self.client_shallow.add(val)
- self.read_proto_line((None,)) # consume client's flush-pkt
- shallow, not_shallow = find_shallow(self.store, wants, depth)
- # Update self.shallow instead of reassigning it since we passed a
- # reference to it before this method was called.
- self.shallow.update(shallow - not_shallow)
- new_shallow = self.shallow - self.client_shallow
- unshallow = self.unshallow = not_shallow & self.client_shallow
- self.update_shallow(new_shallow, unshallow)
- def update_shallow(
- self, new_shallow: AbstractSet[bytes], unshallow: AbstractSet[bytes]
- ) -> None:
- """Update shallow/unshallow information to the client.
- Args:
- new_shallow: Set of newly shallow commits
- unshallow: Set of commits to unshallow
- """
- for sha in sorted(new_shallow):
- self.proto.write_pkt_line(format_shallow_line(sha))
- for sha in sorted(unshallow):
- self.proto.write_pkt_line(format_unshallow_line(sha))
- self.proto.write_pkt_line(None)
- def notify_done(self) -> None:
- """Notify that the client sent 'done'."""
- # relay the message down to the handler.
- self.handler.notify_done()
- def send_ack(self, sha: bytes, ack_type: bytes = b"") -> None:
- """Send an ACK to the client.
- Args:
- sha: SHA to acknowledge
- ack_type: Type of ACK (e.g., b'continue', b'ready')
- """
- self.proto.write_pkt_line(format_ack_line(sha, ack_type))
- def send_nak(self) -> None:
- """Send a NAK to the client."""
- self.proto.write_pkt_line(NAK_LINE)
- def handle_done(self, done_required: bool, done_received: bool) -> bool:
- """Handle the 'done' command.
- Args:
- done_required: Whether done is required
- done_received: Whether done was received
- Returns: True if done handling succeeded
- """
- # Delegate this to the implementation.
- assert self._impl is not None
- return self._impl.handle_done(done_required, done_received)
- def set_wants(self, wants: list[bytes]) -> None:
- """Set the list of wanted objects.
- Args:
- wants: List of wanted object SHAs
- """
- self._wants = wants
- def all_wants_satisfied(self, haves: AbstractSet[bytes]) -> bool:
- """Check whether all the current wants are satisfied by a set of haves.
- Args:
- haves: A set of commits we know the client has.
- Note: Wants are specified with set_wants rather than passed in since
- in the current interface they are determined outside this class.
- """
- return _all_wants_satisfied(self.store, haves, set(self._wants))
- def set_ack_type(self, ack_type: int) -> None:
- """Set the acknowledgment type for the graph walker.
- Args:
- ack_type: One of SINGLE_ACK, MULTI_ACK, or MULTI_ACK_DETAILED
- """
- impl_classes: dict[int, type[AckGraphWalkerImpl]] = {
- MULTI_ACK: MultiAckGraphWalkerImpl,
- MULTI_ACK_DETAILED: MultiAckDetailedGraphWalkerImpl,
- SINGLE_ACK: SingleAckGraphWalkerImpl,
- }
- self._impl = impl_classes[ack_type](self)
- _GRAPH_WALKER_COMMANDS = (COMMAND_HAVE, COMMAND_DONE, None)
- class SingleAckGraphWalkerImpl(AckGraphWalkerImpl):
- """Graph walker implementation that speaks the single-ack protocol."""
- def __init__(self, walker: "_ProtocolGraphWalker") -> None:
- """Initialize a SingleAckGraphWalkerImpl.
- Args:
- walker: Parent ProtocolGraphWalker instance
- """
- self.walker = walker
- self._common: list[bytes] = []
- def ack(self, have_ref: bytes) -> 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) -> bytes | None:
- """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
- self.walker.notify_done()
- return None
- elif command == COMMAND_HAVE:
- assert isinstance(sha, bytes)
- return sha
- return None
- __next__ = next
- def handle_done(self, done_required: bool, done_received: bool) -> 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()
- 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
- # the done.
- return False
- if not done_received and not self._common:
- # Okay we are not actually done then since the walker picked
- # up no haves. This is usually triggered when client attempts
- # to pull from a source that has no common base_commit.
- # See: test_server.MultiAckDetailedGraphWalkerImplTestCase.\
- # test_multi_ack_stateless_nodone
- return False
- return True
- class MultiAckGraphWalkerImpl(AckGraphWalkerImpl):
- """Graph walker implementation that speaks the multi-ack protocol."""
- def __init__(self, walker: "_ProtocolGraphWalker") -> 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: bytes) -> 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")
- if self.walker.all_wants_satisfied(set(self._common)):
- self._found_base = True
- # else we blind ack within next
- def next(self) -> bytes | None:
- """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:
- self.walker.send_nak()
- # in multi-ack mode, a flush-pkt indicates the client wants to
- # flush but more have lines are still coming
- continue
- elif command == COMMAND_DONE:
- self.walker.notify_done()
- return None
- elif command == COMMAND_HAVE:
- assert isinstance(sha, bytes)
- if self._found_base:
- # blind ack
- self.walker.send_ack(sha, b"continue")
- return sha
- __next__ = next
- def handle_done(self, done_required: bool, done_received: bool) -> 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
- # the done.
- return False
- if not done_received and not self._common:
- # Okay we are not actually done then since the walker picked
- # up no haves. This is usually triggered when client attempts
- # to pull from a source that has no common base_commit.
- # See: test_server.MultiAckDetailedGraphWalkerImplTestCase.\
- # test_multi_ack_stateless_nodone
- return False
- # don't nak unless no common commits were found, even if not
- # everything is satisfied
- if self._common:
- self.walker.send_ack(self._common[-1])
- else:
- self.walker.send_nak()
- return True
- class MultiAckDetailedGraphWalkerImpl(AckGraphWalkerImpl):
- """Graph walker implementation speaking the multi-ack-detailed protocol."""
- def __init__(self, walker: "_ProtocolGraphWalker") -> None:
- """Initialize multi-ack-detailed graph walker.
- Args:
- walker: Parent ProtocolGraphWalker instance
- """
- self.walker = walker
- self._common: list[bytes] = []
- def ack(self, have_ref: bytes) -> 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) -> bytes | None:
- """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:
- if self.walker.all_wants_satisfied(set(self._common)):
- self.walker.send_ack(self._common[-1], b"ready")
- self.walker.send_nak()
- if self.walker.stateless_rpc:
- # The HTTP version of this request a flush-pkt always
- # signifies an end of request, so we also return
- # nothing here as if we are done (but not really, as
- # it depends on whether no-done capability was
- # specified and that's handled in handle_done which
- # may or may not call post_nodone_check depending on
- # that).
- return None
- elif command == COMMAND_DONE:
- # Let the walker know that we got a done.
- self.walker.notify_done()
- break
- elif command == COMMAND_HAVE:
- # return the sha and let the caller ACK it with the
- # above ack method.
- assert isinstance(sha, bytes)
- return sha
- # don't nak unless no common commits were found, even if not
- # everything is satisfied
- return None
- __next__ = next
- def handle_done(self, done_required: bool, done_received: bool) -> 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
- # the done.
- return False
- if not done_received and not self._common:
- # Okay we are not actually done then since the walker picked
- # up no haves. This is usually triggered when client attempts
- # to pull from a source that has no common base_commit.
- # See: test_server.MultiAckDetailedGraphWalkerImplTestCase.\
- # test_multi_ack_stateless_nodone
- return False
- # don't nak unless no common commits were found, even if not
- # everything is satisfied
- if self._common:
- self.walker.send_ack(self._common[-1])
- else:
- self.walker.send_nak()
- return True
- class ReceivePackHandler(PackHandler):
- """Protocol handler for downloading a pack from the client."""
- def __init__(
- self,
- backend: Backend,
- args: Sequence[str],
- proto: Protocol,
- stateless_rpc: bool = False,
- advertise_refs: bool = 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,
- CAPABILITY_QUIET,
- CAPABILITY_OFS_DELTA,
- CAPABILITY_SIDE_BAND_64K,
- CAPABILITY_NO_DONE,
- ]
- 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,
- ChecksumMismatch,
- ApplyDeltaError,
- AssertionError,
- socket.error,
- zlib.error,
- ObjectFormatException,
- )
- will_send_pack = False
- for command in refs:
- if command[1] != ZERO_SHA:
- will_send_pack = True
- if will_send_pack:
- # TODO: more informative error messages than just the exception
- # string
- try:
- recv = getattr(self.proto, "recv", None)
- self.repo.object_store.add_thin_pack(self.proto.read, recv) # type: ignore[attr-defined]
- yield (b"unpack", b"ok")
- except all_exceptions as e:
- yield (b"unpack", str(e).replace("\n", "").encode("utf-8"))
- # The pack may still have been moved in, but it may contain
- # broken objects. We trust a later GC to clean it up.
- else:
- # The git protocol want to find a status entry related to unpack
- # process even if no pack data has been sent.
- yield (b"unpack", b"ok")
- for oldsha, sha, ref in refs:
- ref_status = b"ok"
- try:
- if sha == ZERO_SHA:
- if CAPABILITY_DELETE_REFS not in self.capabilities():
- raise GitProtocolError(
- "Attempted to delete refs without delete-refs capability."
- )
- try:
- self.repo.refs.remove_if_equals(ref, oldsha)
- except all_exceptions:
- ref_status = b"failed to delete"
- else:
- try:
- self.repo.refs.set_if_equals(ref, oldsha, sha)
- except all_exceptions:
- ref_status = b"failed to write"
- except KeyError:
- ref_status = b"bad ref"
- yield (ref, ref_status)
- def _report_status(self, status: Sequence[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)
- )
- write = writer.write
- def flush() -> None:
- writer.flush()
- self.proto.write_pkt_line(None)
- else:
- write = self.proto.write_pkt_line # type: ignore[assignment]
- def flush() -> None:
- pass
- for name, msg in status:
- if name == b"unpack":
- write(b"unpack " + msg + b"\n")
- elif msg == b"ok":
- write(b"ok " + name + b"\n")
- else:
- write(b"ng " + name + b" " + msg + b"\n")
- write(None) # type: ignore
- flush()
- def _on_post_receive(self, client_refs: dict[bytes, tuple[bytes, bytes]]) -> None:
- """Run post-receive hook.
- Args:
- client_refs: Dictionary of ref changes from client
- """
- hook = self.repo.hooks.get("post-receive", None) # type: ignore[attr-defined]
- if not hook:
- return
- try:
- output = hook.execute(client_refs)
- if output:
- self.proto.write_sideband(SIDE_BAND_CHANNEL_PROGRESS, output)
- except HookError as err:
- 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())
- if not refs:
- refs = [(CAPABILITIES_REF, ZERO_SHA)]
- logger.info("Sending capabilities: %s", self.capabilities())
- self.proto.write_pkt_line(
- format_ref_line(
- refs[0][0],
- refs[0][1],
- list(self.capabilities()) + symref_capabilities(symrefs),
- )
- )
- for i in range(1, len(refs)):
- ref = refs[i]
- self.proto.write_pkt_line(format_ref_line(ref[0], ref[1]))
- self.proto.write_pkt_line(None)
- if self.advertise_refs:
- return
- client_refs = []
- ref_line = self.proto.read_pkt_line()
- # if ref is none then client doesn't want to send us anything..
- if ref_line is None:
- return
- ref_line, caps = extract_capabilities(ref_line)
- self.set_client_capabilities(caps)
- # client will now send us a list of (oldsha, newsha, ref)
- while ref_line:
- (oldsha, newsha, ref_name) = ref_line.split()
- client_refs.append((oldsha, newsha, ref_name))
- ref_line = self.proto.read_pkt_line()
- # backend can now deal with this refs and read a pack using self.read
- status = list(self._apply_pack(client_refs))
- self._on_post_receive(client_refs) # type: ignore[arg-type]
- # when we have read all the pack from the client, send a status report
- # if the client asked for it
- if self.has_capability(CAPABILITY_REPORT_STATUS):
- self._report_status(status)
- class UploadArchiveHandler(Handler):
- """Handler for git-upload-archive requests."""
- def __init__(
- self,
- backend: Backend,
- args: Sequence[str],
- proto: Protocol,
- stateless_rpc: bool = 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: bytes) -> None:
- self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, x)
- arguments = []
- for pkt in self.proto.read_pkt_seq():
- (key, value) = pkt.split(b" ", 1)
- if key != b"argument":
- raise GitProtocolError(f"unknown command {key!r}")
- arguments.append(value.rstrip(b"\n"))
- prefix = b""
- format = "tar"
- i = 0
- store: BaseObjectStore = self.repo.object_store
- while i < len(arguments):
- argument = arguments[i]
- if argument == b"--prefix":
- i += 1
- prefix = arguments[i]
- elif argument == b"--format":
- i += 1
- format = arguments[i].decode("ascii")
- else:
- commit_sha = self.repo.refs[argument]
- commit_obj = store[commit_sha]
- assert isinstance(commit_obj, Commit)
- tree_obj = store[commit_obj.tree]
- assert isinstance(tree_obj, Tree)
- tree = tree_obj
- i += 1
- self.proto.write_pkt_line(b"ACK")
- self.proto.write_pkt_line(None)
- for chunk in tar_stream(
- store,
- tree,
- mtime=int(time.time()),
- prefix=prefix,
- format=format,
- ):
- write(chunk)
- self.proto.write_pkt_line(None)
- # Default handler classes for git services.
- DEFAULT_HANDLERS = {
- b"git-upload-pack": UploadPackHandler,
- b"git-receive-pack": ReceivePackHandler,
- b"git-upload-archive": UploadArchiveHandler,
- }
- class TCPGitRequestHandler(socketserver.StreamRequestHandler):
- """TCP request handler for git protocol."""
- def __init__(
- self,
- handlers: dict[bytes, type[Handler]],
- request: socket.socket,
- client_address: tuple[str, int],
- server: socketserver.TCPServer,
- ) -> None:
- """Initialize TCP request handler.
- Args:
- handlers: Dictionary mapping commands to handler classes
- request: Request socket
- client_address: Client address tuple
- server: Server instance
- """
- self.handlers = handlers
- socketserver.StreamRequestHandler.__init__(
- self, request, client_address, server
- )
- 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)
- cls = self.handlers.get(command, None)
- if not callable(cls):
- raise GitProtocolError(f"Invalid service {command!r}")
- h = cls(self.server.backend, args, proto) # type: ignore
- h.handle()
- class TCPGitServer(socketserver.TCPServer):
- """TCP server for git protocol."""
- allow_reuse_address = True
- serve = socketserver.TCPServer.serve_forever
- def _make_handler(
- self,
- request: socket.socket,
- client_address: tuple[str, int],
- server: socketserver.TCPServer,
- ) -> TCPGitRequestHandler:
- """Create request handler instance.
- Args:
- request: Request socket
- client_address: Client address tuple
- server: Server instance
- Returns:
- TCPGitRequestHandler instance
- """
- return TCPGitRequestHandler(self.handlers, request, client_address, server)
- def __init__(
- self,
- backend: Backend,
- listen_addr: str,
- port: int = TCP_GIT_PORT,
- handlers: dict[bytes, type[Handler]] | None = 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)
- self.backend = backend
- logger.info("Listening for TCP connections on %s:%d", listen_addr, port)
- socketserver.TCPServer.__init__(self, (listen_addr, port), self._make_handler)
- def verify_request(
- self,
- request: socket.socket | tuple[bytes, socket.socket],
- client_address: tuple[str, int] | socket.socket,
- ) -> 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: socket.socket | tuple[bytes, socket.socket],
- client_address: tuple[str, int] | socket.socket,
- ) -> 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,
- )
- def main(argv: list[str] = sys.argv) -> None:
- """Entry point for starting a TCP git server."""
- import optparse
- parser = optparse.OptionParser()
- parser.add_option(
- "-l",
- "--listen_address",
- dest="listen_address",
- default="localhost",
- help="Binding IP address.",
- )
- parser.add_option(
- "-p",
- "--port",
- dest="port",
- type=int,
- default=TCP_GIT_PORT,
- help="Binding TCP port.",
- )
- options, args = parser.parse_args(argv)
- log_utils.default_logging_config()
- if len(args) > 1:
- gitdir = args[1]
- else:
- gitdir = "."
- # TODO(jelmer): Support git-daemon-export-ok and --export-all.
- backend = FileSystemBackend(gitdir)
- server = TCPGitServer(backend, options.listen_address, options.port)
- server.serve_forever()
- def serve_command(
- handler_cls: type[Handler],
- argv: list[str] = sys.argv,
- backend: Backend | None = None,
- inf: IO[bytes] | None = None,
- outf: IO[bytes] | None = None,
- ) -> int:
- """Serve a single command.
- This is mostly useful for the implementation of commands used by e.g.
- git+ssh.
- Args:
- handler_cls: `Handler` class to use for the request
- argv: execv-style command-line arguments. Defaults to sys.argv.
- backend: `Backend` to use
- inf: File-like object to read from, defaults to standard input.
- outf: File-like object to write to, defaults to standard output.
- Returns: Exit code for use with sys.exit. 0 on success, 1 on failure.
- """
- if backend is None:
- backend = FileSystemBackend()
- if inf is None:
- inf = sys.stdin.buffer
- if outf is None:
- outf = sys.stdout.buffer
- def send_fn(data: bytes) -> None:
- outf.write(data)
- outf.flush()
- proto = Protocol(inf.read, send_fn)
- handler = handler_cls(backend, argv[1:], proto) # type: ignore[arg-type]
- # FIXME: Catch exceptions and write a single-line summary to outf.
- handler.handle()
- return 0
- def generate_info_refs(repo: "BaseRepo") -> Iterator[bytes]:
- """Generate an info refs file."""
- refs = repo.get_refs()
- return write_info_refs(refs, repo.object_store)
- def generate_objects_info_packs(repo: "BaseRepo") -> Iterator[bytes]:
- """Generate an index for for packs."""
- for pack in repo.object_store.packs:
- yield (b"P " + os.fsencode(pack.data.filename) + b"\n")
- def update_server_info(repo: "BaseRepo") -> None:
- """Generate server info for dumb file access.
- This generates info/refs and objects/info/packs,
- similar to "git update-server-info".
- """
- repo._put_named_file(
- os.path.join("info", "refs"), b"".join(generate_info_refs(repo))
- )
- repo._put_named_file(
- os.path.join("objects", "info", "packs"),
- b"".join(generate_objects_info_packs(repo)),
- )
- if __name__ == "__main__":
- main()
|