Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / diffusion / protocol / DiffusionGitUploadPackWireProtocol.php
blob724c1c83b24c7b821e63cd3b151726a690366b12
1 <?php
3 final class DiffusionGitUploadPackWireProtocol
4 extends DiffusionGitWireProtocol {
6 private $readMode = 'length';
7 private $readBuffer;
8 private $readFrameLength;
9 private $readFrames = array();
11 private $readFrameMode = 'refs';
12 private $refFrames = array();
14 private $readMessages = array();
16 public function willReadBytes($bytes) {
17 if ($this->readBuffer === null) {
18 $this->readBuffer = new PhutilRope();
20 $buffer = $this->readBuffer;
22 $buffer->append($bytes);
24 while (true) {
25 $len = $buffer->getByteLength();
26 switch ($this->readMode) {
27 case 'length':
28 // We're expecting 4 bytes containing the length of the protocol
29 // frame as hexadecimal in ASCII text, like "01ab". Wait until we
30 // see at least 4 bytes on the wire.
31 if ($len < 4) {
32 if ($len > 0) {
33 $bytes = $this->peekBytes($len);
34 if (!preg_match('/^[0-9a-f]+\z/', $bytes)) {
35 throw new Exception(
36 pht(
37 'Bad frame length character in Git protocol ("%s"), '.
38 'expected a 4-digit hexadecimal value encoded as ASCII '.
39 'text.',
40 $bytes));
44 // We can't make any more progress until we get enough bytes, so
45 // we're done with state processing.
46 break 2;
49 $frame_length = $this->readBytes(4);
50 $frame_length = hexdec($frame_length);
52 // Note that the frame length includes the 4 header bytes, so we
53 // usually expect a length of 5 or larger. Frames with length 0
54 // are boundaries.
55 if ($frame_length === 0) {
56 $this->readFrames[] = $this->newProtocolFrame('null', '');
57 } else if ($frame_length >= 1 && $frame_length <= 3) {
58 throw new Exception(
59 pht(
60 'Encountered Git protocol frame with unexpected frame '.
61 'length (%s)!',
62 $frame_length));
63 } else {
64 $this->readFrameLength = $frame_length - 4;
65 $this->readMode = 'frame';
68 break;
69 case 'frame':
70 // We're expecting a protocol frame of a specified length. Note that
71 // it is possible for a frame to have length 0.
73 // We don't have enough bytes yet, so wait for more.
74 if ($len < $this->readFrameLength) {
75 break 2;
78 if ($this->readFrameLength > 0) {
79 $bytes = $this->readBytes($this->readFrameLength);
80 } else {
81 $bytes = '';
84 // Emit a protocol frame.
85 $this->readFrames[] = $this->newProtocolFrame('data', $bytes);
86 $this->readMode = 'length';
87 break;
91 while (true) {
92 switch ($this->readFrameMode) {
93 case 'refs':
94 if (!$this->readFrames) {
95 break 2;
98 foreach ($this->readFrames as $key => $frame) {
99 unset($this->readFrames[$key]);
101 if ($frame['type'] === 'null') {
102 $ref_frames = $this->refFrames;
103 $this->refFrames = array();
105 $ref_frames[] = $frame;
107 $this->readMessages[] = $this->newProtocolRefMessage($ref_frames);
108 $this->readFrameMode = 'passthru';
109 break;
110 } else {
111 $this->refFrames[] = $frame;
115 break;
116 case 'passthru':
117 if (!$this->readFrames) {
118 break 2;
121 $this->readMessages[] = $this->newProtocolDataMessage(
122 $this->readFrames);
123 $this->readFrames = array();
125 break;
129 $wire = array();
130 foreach ($this->readMessages as $key => $message) {
131 $wire[] = $message;
132 unset($this->readMessages[$key]);
134 $wire = implode('', $wire);
136 return $wire;
139 public function willWriteBytes($bytes) {
140 return $bytes;
143 private function readBytes($count) {
144 $buffer = $this->readBuffer;
146 $bytes = $buffer->getPrefixBytes($count);
147 $buffer->removeBytesFromHead($count);
149 return $bytes;
152 private function peekBytes($count) {
153 $buffer = $this->readBuffer;
154 return $buffer->getPrefixBytes($count);
157 private function newProtocolFrame($type, $bytes) {
158 return array(
159 'type' => $type,
160 'length' => strlen($bytes),
161 'bytes' => $bytes,
165 private function newProtocolRefMessage(array $frames) {
166 $head_key = head_key($frames);
167 $last_key = last_key($frames);
169 $capabilities = null;
170 $last_frame = null;
172 $refs = array();
173 foreach ($frames as $key => $frame) {
174 $is_last = ($key === $last_key);
175 if ($is_last) {
176 // This is a "0000" frame at the end of the list of refs, so we pass
177 // it through unmodified after we figure out what the rest of the
178 // frames should look like, below.
179 $last_frame = $frame;
180 continue;
183 $is_first = ($key === $head_key);
185 // Otherwise, we expect a list of:
187 // <hash> <ref-name>\0<capabilities>
188 // <hash> <ref-name>
189 // ...
191 // See T13309. The end of this list (which may be empty if a repository
192 // does not have any refs) has a list of zero or more of these:
194 // shallow <hash>
196 // These entries are present if the repository is a shallow clone
197 // which was made with the "--depth" flag.
199 // Note that "shallow" frames do not advertise capabilities, and if
200 // a repository has only "shallow" frames, capabilities are never
201 // advertised.
203 $bytes = $frame['bytes'];
204 $matches = array();
205 if ($is_first) {
206 $capabilities_pattern = '\0(?P<capabilities>[^\n]+)';
207 } else {
208 $capabilities_pattern = '';
211 $ok = preg_match(
212 '('.
213 '^'.
214 '(?:'.
215 '(?P<hash>[0-9a-f]{40}) (?P<name>[^\0\n]+)'.$capabilities_pattern.
216 '|'.
217 'shallow (?P<shallow>[0-9a-f]{40})'.
218 ')'.
219 '\n'.
220 '\z'.
221 ')',
222 $bytes,
223 $matches);
225 if (!$ok) {
226 if ($is_first) {
227 throw new Exception(
228 pht(
229 'Unexpected "git upload-pack" initial protocol frame: expected '.
230 '"<hash> <name>\0<capabilities>\n", or '.
231 '"shallow <hash>\n", got "%s".',
232 $bytes));
233 } else {
234 throw new Exception(
235 pht(
236 'Unexpected "git upload-pack" protocol frame: expected '.
237 '"<hash> <name>\n", or "shallow <hash>\n", got "%s".',
238 $bytes));
242 if (isset($matches['shallow'])) {
243 $name = null;
244 $hash = $matches['shallow'];
245 $is_shallow = true;
246 } else {
247 $name = $matches['name'];
248 $hash = $matches['hash'];
249 $is_shallow = false;
252 if (isset($matches['capabilities'])) {
253 $capabilities = $matches['capabilities'];
256 $refs[] = array(
257 'hash' => $hash,
258 'name' => $name,
259 'shallow' => $is_shallow,
263 $capabilities = DiffusionGitWireProtocolCapabilities::newFromWireFormat(
264 $capabilities);
266 $ref_list = id(new DiffusionGitWireProtocolRefList())
267 ->setCapabilities($capabilities);
269 foreach ($refs as $ref) {
270 $wire_ref = id(new DiffusionGitWireProtocolRef())
271 ->setHash($ref['hash']);
273 if ($ref['shallow']) {
274 $wire_ref->setIsShallow(true);
275 } else {
276 $wire_ref->setName($ref['name']);
279 $ref_list->addRef($wire_ref);
282 // TODO: Here, we have a structured list of refs. In a future change,
283 // we are free to mutate the structure before flattening it back into
284 // wire format.
286 $refs = $ref_list->getRefs();
288 // Before we write the ref list, sort it for consistency with native
289 // Git output. We may have added, removed, or renamed refs and ended up
290 // with an out-of-order list.
292 $refs = msortv($refs, 'newSortVector');
294 // The first ref we send back includes the capabilities data. Note that if
295 // we send back no refs, we also don't send back capabilities! This is
296 // a little surprising, but is consistent with the native behavior of the
297 // protocol.
299 // Likewise, we don't send back any capabilities if we're sending only
300 // "shallow" frames.
302 $output = array();
303 $is_first = true;
304 foreach ($refs as $ref) {
305 $is_shallow = $ref->getIsShallow();
307 if ($is_shallow) {
308 $result = sprintf(
309 "shallow %s\n",
310 $ref->getHash());
311 } else if ($is_first) {
312 $result = sprintf(
313 "%s %s\0%s\n",
314 $ref->getHash(),
315 $ref->getName(),
316 $ref_list->getCapabilities()->toWireFormat());
317 } else {
318 $result = sprintf(
319 "%s %s\n",
320 $ref->getHash(),
321 $ref->getName());
324 $output[] = $this->newProtocolFrame('data', $result);
325 $is_first = false;
328 $output[] = $last_frame;
330 return $this->newProtocolDataMessage($output);
333 private function newProtocolDataMessage(array $frames) {
334 $message = array();
336 foreach ($frames as $frame) {
337 switch ($frame['type']) {
338 case 'null':
339 $message[] = '0000';
340 break;
341 case 'data':
342 $message[] = sprintf(
343 '%04x%s',
344 $frame['length'] + 4,
345 $frame['bytes']);
346 break;
350 $message = implode('', $message);
352 return $message;