diff --git a/src/applications/diffusion/protocol/DiffusionGitUploadPackWireProtocol.php b/src/applications/diffusion/protocol/DiffusionGitUploadPackWireProtocol.php
index 451ad74c9e..724c1c83b2 100644
--- a/src/applications/diffusion/protocol/DiffusionGitUploadPackWireProtocol.php
+++ b/src/applications/diffusion/protocol/DiffusionGitUploadPackWireProtocol.php
@@ -1,325 +1,355 @@
 <?php
 
 final class DiffusionGitUploadPackWireProtocol
   extends DiffusionGitWireProtocol {
 
   private $readMode = 'length';
   private $readBuffer;
   private $readFrameLength;
   private $readFrames = array();
 
   private $readFrameMode = 'refs';
   private $refFrames = array();
 
   private $readMessages = array();
 
   public function willReadBytes($bytes) {
     if ($this->readBuffer === null) {
       $this->readBuffer = new PhutilRope();
     }
     $buffer = $this->readBuffer;
 
     $buffer->append($bytes);
 
     while (true) {
       $len = $buffer->getByteLength();
       switch ($this->readMode) {
         case 'length':
           // We're expecting 4 bytes containing the length of the protocol
           // frame as hexadecimal in ASCII text, like "01ab". Wait until we
           // see at least 4 bytes on the wire.
           if ($len < 4) {
             if ($len > 0) {
               $bytes = $this->peekBytes($len);
               if (!preg_match('/^[0-9a-f]+\z/', $bytes)) {
                 throw new Exception(
                   pht(
                     'Bad frame length character in Git protocol ("%s"), '.
                     'expected a 4-digit hexadecimal value encoded as ASCII '.
                     'text.',
                     $bytes));
               }
             }
 
             // We can't make any more progress until we get enough bytes, so
             // we're done with state processing.
             break 2;
           }
 
           $frame_length = $this->readBytes(4);
           $frame_length = hexdec($frame_length);
 
           // Note that the frame length includes the 4 header bytes, so we
           // usually expect a length of 5 or larger. Frames with length 0
           // are boundaries.
           if ($frame_length === 0) {
             $this->readFrames[] = $this->newProtocolFrame('null', '');
           } else if ($frame_length >= 1 && $frame_length <= 3) {
             throw new Exception(
               pht(
                 'Encountered Git protocol frame with unexpected frame '.
                 'length (%s)!',
                 $frame_length));
           } else {
             $this->readFrameLength = $frame_length - 4;
             $this->readMode = 'frame';
           }
 
           break;
         case 'frame':
           // We're expecting a protocol frame of a specified length. Note that
           // it is possible for a frame to have length 0.
 
           // We don't have enough bytes yet, so wait for more.
           if ($len < $this->readFrameLength) {
             break 2;
           }
 
           if ($this->readFrameLength > 0) {
             $bytes = $this->readBytes($this->readFrameLength);
           } else {
             $bytes = '';
           }
 
           // Emit a protocol frame.
           $this->readFrames[] = $this->newProtocolFrame('data', $bytes);
           $this->readMode = 'length';
           break;
       }
     }
 
     while (true) {
       switch ($this->readFrameMode) {
         case 'refs':
           if (!$this->readFrames) {
             break 2;
           }
 
           foreach ($this->readFrames as $key => $frame) {
             unset($this->readFrames[$key]);
 
             if ($frame['type'] === 'null') {
               $ref_frames = $this->refFrames;
               $this->refFrames = array();
 
               $ref_frames[] = $frame;
 
               $this->readMessages[] = $this->newProtocolRefMessage($ref_frames);
               $this->readFrameMode = 'passthru';
               break;
             } else {
               $this->refFrames[] = $frame;
             }
           }
 
           break;
         case 'passthru':
           if (!$this->readFrames) {
             break 2;
           }
 
           $this->readMessages[] = $this->newProtocolDataMessage(
             $this->readFrames);
           $this->readFrames = array();
 
           break;
       }
     }
 
     $wire = array();
     foreach ($this->readMessages as $key => $message) {
       $wire[] = $message;
       unset($this->readMessages[$key]);
     }
     $wire = implode('', $wire);
 
     return $wire;
   }
 
   public function willWriteBytes($bytes) {
     return $bytes;
   }
 
   private function readBytes($count) {
     $buffer = $this->readBuffer;
 
     $bytes = $buffer->getPrefixBytes($count);
     $buffer->removeBytesFromHead($count);
 
     return $bytes;
   }
 
   private function peekBytes($count) {
     $buffer = $this->readBuffer;
     return $buffer->getPrefixBytes($count);
   }
 
   private function newProtocolFrame($type, $bytes) {
     return array(
       'type' => $type,
       'length' => strlen($bytes),
       'bytes' => $bytes,
     );
   }
 
   private function newProtocolRefMessage(array $frames) {
     $head_key = head_key($frames);
     $last_key = last_key($frames);
 
     $capabilities = null;
     $last_frame = null;
 
     $refs = array();
     foreach ($frames as $key => $frame) {
       $is_last = ($key === $last_key);
       if ($is_last) {
         // This is a "0000" frame at the end of the list of refs, so we pass
         // it through unmodified after we figure out what the rest of the
         // frames should look like, below.
         $last_frame = $frame;
         continue;
       }
 
       $is_first = ($key === $head_key);
 
       // Otherwise, we expect a list of:
       //
       //   <hash> <ref-name>\0<capabilities>
       //   <hash> <ref-name>
       //   ...
+      //
+      // See T13309. The end of this list (which may be empty if a repository
+      // does not have any refs) has a list of zero or more of these:
+      //
+      //   shallow <hash>
+      //
+      // These entries are present if the repository is a shallow clone
+      // which was made with the "--depth" flag.
+      //
+      // Note that "shallow" frames do not advertise capabilities, and if
+      // a repository has only "shallow" frames, capabilities are never
+      // advertised.
 
       $bytes = $frame['bytes'];
       $matches = array();
       if ($is_first) {
-        $ok = preg_match(
-          '('.
-            '^'.
-            '(?P<hash>[0-9a-f]{40})'.
-            ' '.
-            '(?P<name>[^\0\n]+)'.
-            '\0'.
-            '(?P<capabilities>[^\n]+)'.
-            '\n'.
-            '\z'.
-          ')',
-          $bytes,
-          $matches);
-        if (!$ok) {
+        $capabilities_pattern = '\0(?P<capabilities>[^\n]+)';
+      } else {
+        $capabilities_pattern = '';
+      }
+
+      $ok = preg_match(
+        '('.
+          '^'.
+          '(?:'.
+            '(?P<hash>[0-9a-f]{40}) (?P<name>[^\0\n]+)'.$capabilities_pattern.
+            '|'.
+            'shallow (?P<shallow>[0-9a-f]{40})'.
+          ')'.
+          '\n'.
+          '\z'.
+        ')',
+        $bytes,
+        $matches);
+
+      if (!$ok) {
+        if ($is_first) {
           throw new Exception(
             pht(
               'Unexpected "git upload-pack" initial protocol frame: expected '.
-              '"<hash> <name>\0<capabilities>\n", got "%s".',
+              '"<hash> <name>\0<capabilities>\n", or '.
+              '"shallow <hash>\n", got "%s".',
               $bytes));
-        }
-      } else {
-        $ok = preg_match(
-          '('.
-            '^'.
-            '(?P<hash>[0-9a-f]{40})'.
-            ' '.
-            '(?P<name>[^\0\n]+)'.
-            '\n'.
-            '\z'.
-          ')',
-          $bytes,
-          $matches);
-        if (!$ok) {
+        } else {
           throw new Exception(
             pht(
               'Unexpected "git upload-pack" protocol frame: expected '.
-              '"<hash> <name>\n", got "%s".',
+              '"<hash> <name>\n", or "shallow <hash>\n", got "%s".',
               $bytes));
         }
       }
 
-      $hash = $matches['hash'];
-      $name = $matches['name'];
+      if (isset($matches['shallow'])) {
+        $name = null;
+        $hash = $matches['shallow'];
+        $is_shallow = true;
+      } else {
+        $name = $matches['name'];
+        $hash = $matches['hash'];
+        $is_shallow = false;
+      }
 
-      if ($is_first) {
+      if (isset($matches['capabilities'])) {
         $capabilities = $matches['capabilities'];
       }
 
       $refs[] = array(
         'hash' => $hash,
         'name' => $name,
+        'shallow' => $is_shallow,
       );
     }
 
     $capabilities = DiffusionGitWireProtocolCapabilities::newFromWireFormat(
       $capabilities);
 
     $ref_list = id(new DiffusionGitWireProtocolRefList())
       ->setCapabilities($capabilities);
 
     foreach ($refs as $ref) {
-      $ref_list->addRef(
-        id(new DiffusionGitWireProtocolRef())
-          ->setName($ref['name'])
-          ->setHash($ref['hash']));
+      $wire_ref = id(new DiffusionGitWireProtocolRef())
+        ->setHash($ref['hash']);
+
+      if ($ref['shallow']) {
+        $wire_ref->setIsShallow(true);
+      } else {
+        $wire_ref->setName($ref['name']);
+      }
+
+      $ref_list->addRef($wire_ref);
     }
 
     // TODO: Here, we have a structured list of refs. In a future change,
     // we are free to mutate the structure before flattening it back into
     // wire format.
 
     $refs = $ref_list->getRefs();
 
     // Before we write the ref list, sort it for consistency with native
     // Git output. We may have added, removed, or renamed refs and ended up
     // with an out-of-order list.
 
     $refs = msortv($refs, 'newSortVector');
 
     // The first ref we send back includes the capabilities data. Note that if
     // we send back no refs, we also don't send back capabilities! This is
     // a little surprising, but is consistent with the native behavior of the
     // protocol.
 
+    // Likewise, we don't send back any capabilities if we're sending only
+    // "shallow" frames.
+
     $output = array();
     $is_first = true;
     foreach ($refs as $ref) {
-      if ($is_first) {
+      $is_shallow = $ref->getIsShallow();
+
+      if ($is_shallow) {
+        $result = sprintf(
+          "shallow %s\n",
+          $ref->getHash());
+      } else if ($is_first) {
         $result = sprintf(
           "%s %s\0%s\n",
           $ref->getHash(),
           $ref->getName(),
           $ref_list->getCapabilities()->toWireFormat());
       } else {
         $result = sprintf(
           "%s %s\n",
           $ref->getHash(),
           $ref->getName());
       }
 
       $output[] = $this->newProtocolFrame('data', $result);
       $is_first = false;
     }
 
     $output[] = $last_frame;
 
     return $this->newProtocolDataMessage($output);
   }
 
   private function newProtocolDataMessage(array $frames) {
     $message = array();
 
     foreach ($frames as $frame) {
       switch ($frame['type']) {
         case 'null':
           $message[] = '0000';
           break;
         case 'data':
           $message[] = sprintf(
             '%04x%s',
             $frame['length'] + 4,
             $frame['bytes']);
           break;
       }
     }
 
     $message = implode('', $message);
 
     return $message;
   }
 
 }
diff --git a/src/applications/diffusion/protocol/DiffusionGitWireProtocolRef.php b/src/applications/diffusion/protocol/DiffusionGitWireProtocolRef.php
index bf5238c219..bd1672317a 100644
--- a/src/applications/diffusion/protocol/DiffusionGitWireProtocolRef.php
+++ b/src/applications/diffusion/protocol/DiffusionGitWireProtocolRef.php
@@ -1,32 +1,43 @@
 <?php
 
 final class DiffusionGitWireProtocolRef
   extends Phobject {
 
   private $name;
   private $hash;
+  private $isShallow;
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setHash($hash) {
     $this->hash = $hash;
     return $this;
   }
 
   public function getHash() {
     return $this->hash;
   }
 
+  public function setIsShallow($is_shallow) {
+    $this->isShallow = $is_shallow;
+    return $this;
+  }
+
+  public function getIsShallow() {
+    return $this->isShallow;
+  }
+
   public function newSortVector() {
     return id(new PhutilSortVector())
-      ->addString($this->getName());
+      ->addInt((int)$this->getIsShallow())
+      ->addString((string)$this->getName());
   }
 
 }