<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\library;
/**
* Serves a Git repository to a cloning client over the "smart" side of
* Git's HTTP protocol, in pure PHP with no git program involved. The dumb
* side of that protocol just hands back the repository's stored files, so a
* client has to download the whole history; the smart side lets the client
* ask for only what it wants, which is what makes a shallow clone (git's
* --depth option) possible. This class produces the two smart replies: the
* advertisement a client reads first to learn the repository's branches and
* what the server can do, and the response to the follow-up request in which
* the client says which commits it wants and how much history to include.
*
* It reads objects through a GitRepository, which already knows how to pull
* commits, trees, and blobs out of both loose files and packfiles (undoing
* Git's delta compression on the way). This class adds only the protocol on
* top: framing bytes into Git's length-prefixed "pkt-line" records, reading
* the client's request, walking history from the wanted commits down to the
* requested depth, and writing the chosen objects into a fresh packfile.
* Objects are written into that pack whole rather than as deltas, which is
* always valid and keeps the writer simple. Like the rest of the Git library
* it only ever reads the repository.
*
* The framing follows Git's pack protocol: a pkt-line is a four-hex-digit
* length (counting the four digits) then that many bytes, and the special
* length "0000" is a flush marker that separates sections.
*
* @author Chris Pollett
*/
class GitUploadPack
{
/**
* The repository being served, used to read refs and objects
* @var object
*/
private $repository;
/**
* Object names the client already holds, gathered from the commits it
* says it has and everything those record. Anything in here is left out
* of the packfile, since sending a file the client already has is what
* turns a small update into a copy of the whole working tree.
* @var array
*/
private $held_objects = [];
/**
* For each object being packed, the kind of thing it is and, for a
* folder or file, the name it goes by. Successive versions of one file
* are nearly alike, so packing them next to each other is what lets each
* be sent as its small difference from the one before.
* @var array
*/
private $object_groups = [];
/**
* For each folder and file the client already holds, the object name of
* the version it has, kept under the name that thing goes by. A new
* version can then be sent as its difference from the client's own copy
* instead of whole, which is what the thin-pack capability allows.
* @var array
*/
private $held_by_name = [];
/**
* Length in bytes of the hex count that begins every pkt-line
* @var int
*/
const PKT_LENGTH_DIGITS = 4;
/**
* The flush marker, a pkt-line whose length field is all zeros, which
* separates one section of the protocol from the next
* @var string
*/
const FLUSH_PKT = "0000";
/**
* Largest byte count a single pkt-line can carry, including its own
* four-digit length, fixed by the pack protocol
* @var int
*/
const MAX_PKT_LENGTH = 65520;
/**
* Number of hex characters in an object name
* @var int
*/
const SHA_HEX_LENGTH = 40;
/**
* Pack object type code for a commit, used in a packed object's header
* @var int
*/
const OBJECT_TYPE_COMMIT = 1;
/**
* Pack object type code for a tree
* @var int
*/
const OBJECT_TYPE_TREE = 2;
/**
* Pack object type code for a blob
* @var int
*/
const OBJECT_TYPE_BLOB = 3;
/**
* Pack object type code for an annotated tag
* @var int
*/
const OBJECT_TYPE_TAG = 4;
/**
* Map from Git's object type words to the numeric type codes a packfile
* uses in each object's header
* @var array
*/
const TYPE_CODES = ["commit" => self::OBJECT_TYPE_COMMIT,
"tree" => self::OBJECT_TYPE_TREE, "blob" => self::OBJECT_TYPE_BLOB,
"tag" => self::OBJECT_TYPE_TAG];
/**
* Bits of the object size that fit in the first byte of a packed
* object's header, the low four bits after the three type bits
* @var int
*/
const FIRST_SIZE_BITS = 4;
/**
* Bits of the object size carried by each continuation byte of a packed
* object's header, the low seven bits
* @var int
*/
const CONTINUATION_SIZE_BITS = 7;
/**
* The high bit of a header byte, set to say another size byte follows
* @var int
*/
const CONTINUATION_FLAG = 0x80;
/**
* A cap on how many objects one response may pack, so a request cannot
* make the server walk an unbounded amount of history at once
* @var int
*/
const MAX_PACK_OBJECTS = 250000;
/**
* Pack type code for an object written as its difference from another
* object that the packfile names outright
* @var int
*/
const TYPE_REF_DELTA = 7;
/**
* Most objects in a row that may be written as a difference from the one
* before. Rebuilding the last of such a run means rebuilding all of
* them, so the run is broken every so often by an object written in
* full, keeping that work bounded for the client.
* @var int
*/
const MAX_DELTA_CHAIN = 10;
/**
* The service name a cloning (read-only) client asks for
* @var string
*/
const UPLOAD_PACK_SERVICE = "git-upload-pack";
/**
* The name a client uses to say it will accept a pack whose differences
* may be built against content the client already holds rather than
* against content sent alongside. Such a pack is smaller, and the client
* completes it from its own store on arrival.
* @var string
*/
const THIN_PACK_CAPABILITY = "thin-pack";
/**
* The capabilities the server tells the client it supports. "shallow"
* is the one that allows a depth-limited clone; "no-progress" says the
* server will not send human-readable progress lines;
* "multi_ack_detailed" lets a round of negotiation name each commit the
* two sides are found to hold in common and say when the server is ready
* to send, which is what lets a client with a long history settle
* quickly instead of giving up and asking for all of it; "thin-pack"
* lets a file's new version be sent as its difference from the version
* the client already has, rather than whole. Side-band multiplexing is
* deliberately not offered, so the packfile comes back as plain bytes
* after the acknowledgment rather than chopped into band-tagged records.
* @var string
*/
const CAPABILITIES =
"shallow no-progress multi_ack_detailed thin-pack";
/**
* Makes an upload-pack server for one repository.
*
* @param object $repository the GitRepository whose refs and objects
* are to be served
*/
public function __construct($repository)
{
$this->repository = $repository;
}
/**
* Wraps a piece of data as one pkt-line: its length, counting the four
* length digits themselves, in four hex digits, followed by the data.
*
* @param string $data the bytes to frame
* @return string the framed pkt-line
*/
public static function pktLine($data)
{
$length = strlen($data) + self::PKT_LENGTH_DIGITS;
return sprintf("%04x", $length) . $data;
}
/**
* Splits a run of pkt-lines into the pieces of data they carry. A flush
* marker is reported as a boolean false entry so a reader can see where
* one section ends, since the protocol uses flushes to separate the list
* of wants from the list of the client's commits.
*
* @param string $stream the bytes to read, a sequence of pkt-lines
* @return array in-order contents of each line, with false marking a
* flush; the trailing newline Git puts on a line is trimmed
*/
public static function decodePktLines($stream)
{
$lines = [];
$offset = 0;
$total = strlen($stream);
while ($offset + self::PKT_LENGTH_DIGITS <= $total) {
$length = hexdec(substr($stream, $offset,
self::PKT_LENGTH_DIGITS));
if ($length == 0) {
$lines[] = false;
$offset += self::PKT_LENGTH_DIGITS;
continue;
}
if ($length < self::PKT_LENGTH_DIGITS ||
$offset + $length > $total) {
break;
}
$content = substr($stream, $offset + self::PKT_LENGTH_DIGITS,
$length - self::PKT_LENGTH_DIGITS);
$lines[] = rtrim($content, "\n");
$offset += $length;
}
return $lines;
}
/**
* Builds the advertisement a client reads first, over a GET, to discover
* the repository. It names the service, then lists every ref with the
* object it points at, attaching the server's capabilities to the first
* line, and points HEAD at the default branch so the client knows which
* branch to check out. Annotated tags also get a peeled line giving the
* commit the tag ultimately refers to.
*
* @return string the advertisement as pkt-lines, ready to send as the
* body of the info/refs reply
*/
public function advertiseRefs()
{
$output = self::pktLine("# service=" . self::UPLOAD_PACK_SERVICE .
"\n") . self::FLUSH_PKT;
$refs = $this->repository->allRefs();
$head_branch = $this->repository->headBranch();
$head_line = "";
if ($head_branch != "" &&
isset($refs["refs/heads/" . $head_branch])) {
$head_line = $refs["refs/heads/" . $head_branch] . " HEAD";
}
$capabilities = self::CAPABILITIES;
if ($head_line != "") {
$capabilities .= " symref=HEAD:refs/heads/" . $head_branch;
}
$first = true;
if ($head_line != "") {
$output .= self::pktLine($head_line . "\0" . $capabilities .
"\n");
$first = false;
}
foreach ($refs as $name => $object_name) {
$line = $object_name . " " . $name;
if ($first) {
$output .= self::pktLine($line . "\0" . $capabilities .
"\n");
$first = false;
} else {
$output .= self::pktLine($line . "\n");
}
$peeled = $this->repository->peeledTarget($object_name);
if ($peeled != "") {
$output .= self::pktLine($peeled . " " . $name . "^{}\n");
}
}
return $output . self::FLUSH_PKT;
}
/**
* Reads the client's upload request out of the pkt-lines it posted. The
* client lists the object names it wants, the object names it already has
* (its current tips, so the server can leave their history out), may say
* how deep a history to send, and ends with "done." Any capabilities the
* client tacks onto its first want line are ignored here, since the
* server only offers a fixed set.
*
* @param string $request_body the raw posted body
* @return array ["wants" => list of wanted object names,
* "haves" => list of object names the client already has,
* "depth" => requested history depth, 0 meaning the whole history,
* "done" => whether the client said it is done negotiating]
*/
public function parseUploadRequest($request_body)
{
$wants = [];
$haves = [];
$capabilities = [];
$depth = 0;
$done = false;
foreach (self::decodePktLines($request_body) as $line) {
if ($line === false || $line === "") {
continue;
}
$parts = explode(" ", $line);
if ($parts[0] == "want" && isset($parts[1]) &&
$this->isObjectName($parts[1])) {
$wants[$parts[1]] = true;
foreach (array_slice($parts, 2) as $capability) {
$capabilities[trim($capability)] = true;
}
} else if ($parts[0] == "have" && isset($parts[1]) &&
$this->isObjectName($parts[1])) {
$haves[$parts[1]] = true;
} else if ($parts[0] == "deepen" && isset($parts[1])) {
$depth = max(0, intval($parts[1]));
} else if ($parts[0] == "done") {
$done = true;
}
}
return ["wants" => array_keys($wants),
"haves" => array_keys($haves), "depth" => $depth,
"done" => $done,
"thin" => isset($capabilities[self::THIN_PACK_CAPABILITY])];
}
/**
* Produces the reply to the client's upload request. A depth-limited
* clone is negotiated in two exchanges over stateless HTTP: first the
* client sends its wants and depth without saying "done," and is told
* only where history is cut off (the shallow-boundary lines then a
* flush); then it sends the same wants with "done," and is sent the
* acknowledgment and the packfile. A request that already says "done,"
* such as a full clone with no depth, is answered with the pack in one
* go. The depth-limited walk stops after the given number of commits
* along each line of history and reports the commits where it stopped as
* "shallow," so the client knows its copy of history is cut off there.
*
* @param string $request_body the client's posted upload request
* @return string the reply bytes: either the shallow-boundary lines and
* a flush on their own, or those followed by an acknowledgment and
* the packfile
*/
public function uploadPack($request_body)
{
$reply = "";
foreach ($this->uploadPackChunks($request_body) as $chunk) {
$reply .= $chunk;
}
return $reply;
}
/**
* Produces the reply to a client's clone or fetch request a piece at a
* time, yielding each as it is made rather than building the whole thing
* first.
*
* Yielding is what keeps a large clone from taking the server down with
* it: the caller hands this to the web server, which advances it only as
* the client reads, so the packfile is neither held in memory nor built
* in one long stretch that leaves every other connection unserved. It
* also means the reply's length is not known when the headers go out, so
* the body is sent without a stated length.
*
* A fetch happens in rounds. The client names what it wants and lists
* commits it already has; while nothing it lists is recognized the reply
* is a bare "NAK" and the client offers older commits next. As soon as
* one is recognized the reply names it as held in common and says the
* server is ready, which stops the client walking further back. Once the
* client says it is done, the reply is a final acknowledgment followed
* by the packfile of just the objects it is missing. Sending the pack
* before that leaves the client reading pack bytes where it expects the
* next pkt-line.
*
* @param string $request_body the client's posted upload request
* @return \Generator yields the reply's bytes in order
*/
public function uploadPackChunks($request_body)
{
$request = $this->parseUploadRequest($request_body);
$commons = $this->commonCommits($request["haves"]);
if (!$request["done"] && $request["depth"] <= 0) {
yield from $this->negotiationRoundChunks($commons);
return;
}
$walk = $this->walkWanted($request["wants"], $request["depth"],
$request["haves"]);
if ($request["depth"] > 0) {
foreach ($walk["shallow"] as $shallow_sha) {
yield self::pktLine("shallow " . $shallow_sha . "\n");
}
yield self::FLUSH_PKT;
if (!$request["done"]) {
return;
}
}
if (empty($commons)) {
yield self::pktLine("NAK\n");
} else {
yield self::pktLine("ACK " . $commons[0] . "\n");
}
yield from $this->packChunks($walk["objects"],
$request["thin"]);
}
/**
* Produces the answer to one round of a client's offers of commits it
* already has. Naming each recognized commit as held in common, and then
* saying the server is ready, is what lets the client stop offering ever
* older commits and ask for its update; without it a client with a long
* history gives up after a couple of hundred offers, concludes the two
* sides share nothing, and asks for the whole history. The closing "NAK"
* ends the round: a flush in its place is read as a missing
* acknowledgment, and no closing line at all leaves the client waiting.
*
* @param array $commons object names from this round that the repository
* also holds, newest first
* @return \Generator yields the round's pkt-lines in order
*/
private function negotiationRoundChunks($commons)
{
foreach ($commons as $common) {
yield self::pktLine("ACK " . $common . " common\n");
}
if (!empty($commons)) {
yield self::pktLine("ACK " . $commons[0] . " ready\n");
}
yield self::pktLine("NAK\n");
}
/**
* Picks out the commits a client says it already has that this
* repository also holds. A client lists its commits newest first, so the
* result keeps that order and its first entry is the newest commit the
* two sides share.
*
* @param array $haves object names the client says it has
* @return array the object names of those this repository also holds
*/
private function commonCommits($haves)
{
$commons = [];
foreach ($haves as $sha) {
if ($this->repository->readObject($sha) !== null) {
$commons[] = $sha;
}
}
return $commons;
}
/**
* Walks history from the wanted commits and gathers every object a
* client needs to check them out: the commits themselves and, for each,
* the whole tree of folders and files it records. When a depth is given
* the walk keeps only that many commits deep along each parent line and
* marks the commits at the cutoff as shallow. A wanted object that is a
* tag is followed to the commit it names first.
*
* @param array $wants object names the client asked for
* @param int $depth most commits to include along each history line, or
* 0 for no limit
* @param array $haves object names the client already has; the walk
* treats these as already visited so it stops at them and leaves
* their history out of the pack
* @return array ["objects" => ordered list of object names to pack,
* "shallow" => object names of commits at the depth cutoff]
*/
private function walkWanted($wants, $depth, $haves = [])
{
$objects = [];
$shallow = [];
$seen_commits = [];
$this->held_objects = [];
$this->held_by_name = [];
foreach ($haves as $have_sha) {
$seen_commits[$have_sha] = true;
$this->collectHeld($have_sha);
}
$queue = [];
foreach ($wants as $want) {
$commit_sha = $this->resolveToCommit($want, $objects);
if ($commit_sha != "") {
$queue[] = ["sha" => $commit_sha, "level" => 1];
}
}
while (!empty($queue)) {
$item = array_shift($queue);
$commit_sha = $item["sha"];
if (isset($seen_commits[$commit_sha])) {
continue;
}
$seen_commits[$commit_sha] = true;
$this->addObject($commit_sha, $objects);
$commit = $this->repository->commit($commit_sha);
$this->collectTree($commit["tree"], $objects, "");
$at_cutoff = ($depth > 0 && $item["level"] >= $depth);
if ($at_cutoff && !empty($commit["parents"])) {
$shallow[$commit_sha] = true;
continue;
}
foreach ($commit["parents"] as $parent_sha) {
$queue[] = ["sha" => $parent_sha,
"level" => $item["level"] + 1];
}
}
return ["objects" => array_keys($objects),
"shallow" => array_keys($shallow)];
}
/**
* Follows a wanted object name to the commit it stands for. A commit is
* returned as itself; an annotated tag is unwrapped, adding the tag
* object to the pack on the way, until a commit is reached. Anything
* that is not a commit or tag yields the empty string.
*
* @param string $sha the wanted object name
* @param array &$objects running set of objects to pack, added to when
* a tag is unwrapped
* @return string the commit's object name, or "" if none is reached
*/
private function resolveToCommit($sha, &$objects)
{
$steps = 0;
while ($steps < self::MAX_PACK_OBJECTS) {
$steps++;
$object = $this->repository->readObject($sha);
if ($object === null) {
return "";
}
if ($object["type"] == "commit") {
return $sha;
}
if ($object["type"] != "tag") {
return "";
}
$this->addObject($sha, $objects);
$target = "";
foreach (explode("\n", $object["body"]) as $line) {
if (strncmp($line, "object ", 7) === 0) {
$target = trim(substr($line, 7));
break;
}
}
if (!$this->isObjectName($target)) {
return "";
}
$sha = $target;
}
return "";
}
/**
* Adds a tree and everything under it to the set of objects to pack: the
* tree object itself, each blob it lists, and, by walking into them, all
* sub-trees. A tree already added is skipped, so shared folders are
* packed once.
*
* @param string $tree_sha object name of the tree to collect
* @param array &$objects running set of objects to pack
* @param string $name path this folder goes by, used to set successive
* versions of one file next to each other when packing
*/
private function collectTree($tree_sha, &$objects,
$name = "")
{
if (isset($objects[$tree_sha]) ||
isset($this->held_objects[$tree_sha])) {
return;
}
$this->addObject($tree_sha, $objects, "tree:" . $name);
foreach ($this->repository->tree($tree_sha) as $entry) {
$entry_name = $name . "/" . $entry["name"];
if ($entry["is_dir"]) {
$this->collectTree($entry["sha"], $objects, $entry_name);
} else {
$this->addObject($entry["sha"], $objects,
"blob:" . $entry_name);
}
}
}
/**
* Records one object name in the set to pack, keeping insertion order and
* refusing to grow past the object cap.
*
* @param string $sha object name to add
* @param array &$objects running set of objects to pack
* @param string $group what the object is and the name it goes by, used
* to set successive versions of one file next to each other
*/
private function addObject($sha, &$objects, $group = "")
{
if (!isset($objects[$sha]) && !isset($this->held_objects[$sha]) &&
count($objects) < self::MAX_PACK_OBJECTS) {
$objects[$sha] = true;
$this->object_groups[$sha] = $group;
}
}
/**
* Gathers everything one commit the client says it has records: the
* commit, the folders it names, and the files in them. A folder already
* gathered is skipped, so walking several of the client's commits costs
* little more than walking one, neighboring commits sharing most of
* their folders. A name the repository does not hold, or that is not a
* commit, is passed over.
*
* @param string $commit_sha object name of a commit the client has
*/
private function collectHeld($commit_sha)
{
$object = $this->repository->readObject($commit_sha);
if ($object === null || $object["type"] != "commit") {
return;
}
$this->held_objects[$commit_sha] = true;
$commit = $this->repository->commit($commit_sha);
$this->collectHeldTree($commit["tree"], "");
}
/**
* Gathers a folder the client already has and everything under it: the
* folder itself, each file it lists, and, by walking into them, all its
* sub-folders.
*
* @param string $tree_sha object name of the folder to gather
* @param string $name path this folder goes by, so the version of each
* thing the client holds can be found again by name
*/
private function collectHeldTree($tree_sha, $name = "")
{
if (isset($this->held_objects[$tree_sha])) {
return;
}
$this->held_objects[$tree_sha] = true;
$this->held_by_name["tree:" . $name] = $tree_sha;
foreach ($this->repository->tree($tree_sha) as $entry) {
$entry_name = $name . "/" . $entry["name"];
if ($entry["is_dir"]) {
$this->collectHeldTree($entry["sha"], $entry_name);
} else {
$this->held_objects[$entry["sha"]] = true;
$this->held_by_name["blob:" . $entry_name] = $entry["sha"];
}
}
}
/**
* Produces a packfile of the given objects a piece at a time: the "PACK"
* signature, the count of objects, then each object, and finally a
* checksum of everything before it. The checksum is kept running as the
* pieces go out, so the file never has to be assembled first in order to
* be summed, and the caller can send each piece as it arrives.
*
* Objects are put in an order that sets successive versions of the same
* file next to each other, and each is then written as its difference
* from the one before it where that comes out smaller. Since two
* versions of a file usually share nearly all their bytes, this is what
* keeps a clone from carrying every version at full size. A run of such
* differences is broken every so often by a version written in full, so
* that rebuilding any one of them never means following a long chain.
*
* @param array $object_names object names to write
* @param bool $thin whether the client will accept differences built
* against content it already holds and completes the pack itself
* @return \Generator yields the packfile's bytes in order
*/
private function packChunks($object_names, $thin = false)
{
$ordered = $this->orderForPack($object_names);
$checksum = hash_init("sha1");
$header = "PACK" . pack("N", 2) . pack("N", count($ordered));
hash_update($checksum, $header);
yield $header;
$previous = null;
$chain = 0;
foreach ($ordered as $sha) {
$object = $this->repository->readObject($sha);
$group = $this->object_groups[$sha] ?? "";
$delta = null;
$base_sha = "";
if ($previous !== null && $previous["group"] === $group &&
$chain < self::MAX_DELTA_CHAIN) {
$delta = GitDelta::create($previous["body"],
$object["body"]);
$base_sha = $previous["sha"];
}
if ($delta === null && $thin) {
$held = $this->heldVersion($group);
if ($held !== null) {
$delta = GitDelta::create($held["body"],
$object["body"]);
$base_sha = $held["sha"];
}
}
if ($delta === null) {
$chain = 0;
$type_code = self::TYPE_CODES[$object["type"]] ?? 0;
$piece = $this->packObjectHeader($type_code,
strlen($object["body"])) . gzcompress($object["body"]);
} else {
$chain++;
$piece = $this->packObjectHeader(self::TYPE_REF_DELTA,
strlen($delta)) . hex2bin($base_sha) .
gzcompress($delta);
}
hash_update($checksum, $piece);
yield $piece;
$previous = ["sha" => $sha, "body" => $object["body"],
"group" => $group];
}
yield hash_final($checksum, true);
}
/**
* Finds the version of a folder or file that the client already holds,
* so a new version can be sent as its difference from that rather than
* whole. Only used when the client has said it accepts such a pack, as
* the content built against is not sent alongside.
*
* @param string $group what the object is and the name it goes by
* @return array|null ["sha" => object name of the client's version,
* "body" => its content], or null if the client holds no version
*/
private function heldVersion($group)
{
if (!isset($this->held_by_name[$group])) {
return null;
}
$sha = $this->held_by_name[$group];
$object = $this->repository->readObject($sha);
if ($object === null) {
return null;
}
return ["sha" => $sha, "body" => $object["body"]];
}
/**
* Puts the objects to be packed in an order that sets successive
* versions of the same file, or the same folder, next to each other.
* Commits keep the order history was walked in and come first; folders
* and files follow, gathered by the name they go by. Only neighbors are
* compared when packing, so this ordering is what decides which
* differences can be found.
*
* @param array $object_names object names to put in order
* @return array the same object names, ordered for packing
*/
private function orderForPack($object_names)
{
$commits = [];
$by_group = [];
foreach ($object_names as $sha) {
$group = $this->object_groups[$sha] ?? "";
if ($group === "") {
$commits[] = $sha;
continue;
}
if (!isset($by_group[$group])) {
$by_group[$group] = [];
}
$by_group[$group][] = $sha;
}
ksort($by_group);
$ordered = $commits;
foreach ($by_group as $group_names) {
foreach ($group_names as $sha) {
$ordered[] = $sha;
}
}
return $ordered;
}
/**
* Builds the variable-length header that precedes a packed object. The
* first byte holds the type and the lowest four bits of the size; each
* further byte holds seven more bits of the size, low bits first, with
* the high bit set on every byte but the last to say more follow.
*
* @param int $type_code the object's pack type code
* @param int $size the object's uncompressed size in bytes
* @return string the encoded header
*/
private function packObjectHeader($type_code, $size)
{
$first = ($type_code << self::FIRST_SIZE_BITS) |
($size & ((1 << self::FIRST_SIZE_BITS) - 1));
$size = $size >> self::FIRST_SIZE_BITS;
$header = "";
while ($size > 0) {
$header .= chr($first | self::CONTINUATION_FLAG);
$first = $size & ((1 << self::CONTINUATION_SIZE_BITS) - 1);
$size = $size >> self::CONTINUATION_SIZE_BITS;
}
return $header . chr($first);
}
/**
* Reports whether a string is a well-formed object name: forty
* hexadecimal characters.
*
* @param string $sha the string to check
* @return bool true if it is a valid object name
*/
private function isObjectName($sha)
{
return is_string($sha) && strlen($sha) == self::SHA_HEX_LENGTH &&
ctype_xdigit($sha);
}
}