diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php
index 31bcb2cbe..80fb22416 100644
--- a/src/applications/diffusion/controller/DiffusionServeController.php
+++ b/src/applications/diffusion/controller/DiffusionServeController.php
@@ -1,1136 +1,1183 @@
 <?php
 
 final class DiffusionServeController extends DiffusionController {
 
   private $serviceViewer;
   private $serviceRepository;
 
   private $isGitLFSRequest;
   private $gitLFSToken;
+  private $gitLFSInput;
 
   public function setServiceViewer(PhabricatorUser $viewer) {
     $this->getRequest()->setUser($viewer);
 
     $this->serviceViewer = $viewer;
     return $this;
   }
 
   public function getServiceViewer() {
     return $this->serviceViewer;
   }
 
   public function setServiceRepository(PhabricatorRepository $repository) {
     $this->serviceRepository = $repository;
     return $this;
   }
 
   public function getServiceRepository() {
     return $this->serviceRepository;
   }
 
   public function getIsGitLFSRequest() {
     return $this->isGitLFSRequest;
   }
 
   public function getGitLFSToken() {
     return $this->gitLFSToken;
   }
 
   public function isVCSRequest(AphrontRequest $request) {
     $identifier = $this->getRepositoryIdentifierFromRequest($request);
     if ($identifier === null) {
       return null;
     }
 
     $content_type = $request->getHTTPHeader('Content-Type');
     $user_agent = idx($_SERVER, 'HTTP_USER_AGENT');
     $request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
 
     // This may have a "charset" suffix, so only match the prefix.
     $lfs_pattern = '(^application/vnd\\.git-lfs\\+json(;|\z))';
 
     $vcs = null;
     if ($request->getExists('service')) {
       $service = $request->getStr('service');
       // We get this initially for `info/refs`.
       // Git also gives us a User-Agent like "git/1.8.2.3".
       $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
     } else if (strncmp($user_agent, 'git/', 4) === 0) {
       $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
     } else if ($content_type == 'application/x-git-upload-pack-request') {
       // We get this for `git-upload-pack`.
       $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
     } else if ($content_type == 'application/x-git-receive-pack-request') {
       // We get this for `git-receive-pack`.
       $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
     } else if (preg_match($lfs_pattern, $content_type)) {
       // This is a Git LFS HTTP API request.
       $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
       $this->isGitLFSRequest = true;
     } else if ($request_type == 'git-lfs') {
       // This is a Git LFS object content request.
       $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
       $this->isGitLFSRequest = true;
     } else if ($request->getExists('cmd')) {
       // Mercurial also sends an Accept header like
       // "application/mercurial-0.1", and a User-Agent like
       // "mercurial/proto-1.0".
       $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
     } else {
       // Subversion also sends an initial OPTIONS request (vs GET/POST), and
       // has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2)
       // serf/1.3.2".
       $dav = $request->getHTTPHeader('DAV');
       $dav = new PhutilURI($dav);
       if ($dav->getDomain() === 'subversion.tigris.org') {
         $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
       }
     }
 
     return $vcs;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $service_exception = null;
     $response = null;
 
     try {
       $response = $this->serveRequest($request);
     } catch (Exception $ex) {
       $service_exception = $ex;
     }
 
     try {
       $remote_addr = $request->getRemoteAddress();
 
       $pull_event = id(new PhabricatorRepositoryPullEvent())
         ->setEpoch(PhabricatorTime::getNow())
         ->setRemoteAddress($remote_addr)
         ->setRemoteProtocol('http');
 
       if ($response) {
         $pull_event
           ->setResultType('wild')
           ->setResultCode($response->getHTTPResponseCode());
 
         if ($response instanceof PhabricatorVCSResponse) {
           $pull_event->setProperties(
             array(
               'response.message' => $response->getMessage(),
             ));
         }
       } else {
         $pull_event
           ->setResultType('exception')
           ->setResultCode(500)
           ->setProperties(
             array(
               'exception.class' => get_class($ex),
               'exception.message' => $ex->getMessage(),
             ));
       }
 
       $viewer = $this->getServiceViewer();
       if ($viewer) {
         $pull_event->setPullerPHID($viewer->getPHID());
       }
 
       $repository = $this->getServiceRepository();
       if ($repository) {
         $pull_event->setRepositoryPHID($repository->getPHID());
       }
 
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
         $pull_event->save();
       unset($unguarded);
 
     } catch (Exception $ex) {
       if ($service_exception) {
         throw $service_exception;
       }
       throw $ex;
     }
 
     if ($service_exception) {
       throw $service_exception;
     }
 
     return $response;
   }
 
   private function serveRequest(AphrontRequest $request) {
     $identifier = $this->getRepositoryIdentifierFromRequest($request);
 
     // If authentication credentials have been provided, try to find a user
     // that actually matches those credentials.
     if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
       $username = $_SERVER['PHP_AUTH_USER'];
       $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']);
 
       // Try Git LFS auth first since we can usually reject it without doing
       // any queries, since the username won't match the one we expect or the
       // request won't be LFS.
       $viewer = $this->authenticateGitLFSUser($username, $password);
 
       // If that failed, try normal auth. Note that we can use normal auth on
       // LFS requests, so this isn't strictly an alternative to LFS auth.
       if (!$viewer) {
         $viewer = $this->authenticateHTTPRepositoryUser($username, $password);
       }
 
       if (!$viewer) {
         return new PhabricatorVCSResponse(
           403,
           pht('Invalid credentials.'));
       }
     } else {
       // User hasn't provided credentials, which means we count them as
       // being "not logged in".
       $viewer = new PhabricatorUser();
     }
 
     $this->setServiceViewer($viewer);
 
     $allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
     $allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
     if (!$allow_public) {
       if (!$viewer->isLoggedIn()) {
         if ($allow_auth) {
           return new PhabricatorVCSResponse(
             401,
             pht('You must log in to access repositories.'));
         } else {
           return new PhabricatorVCSResponse(
             403,
             pht('Public and authenticated HTTP access are both forbidden.'));
         }
       }
     }
 
     try {
       $repository = id(new PhabricatorRepositoryQuery())
         ->setViewer($viewer)
         ->withIdentifiers(array($identifier))
         ->executeOne();
       if (!$repository) {
         return new PhabricatorVCSResponse(
           404,
           pht('No such repository exists.'));
       }
     } catch (PhabricatorPolicyException $ex) {
       if ($viewer->isLoggedIn()) {
         return new PhabricatorVCSResponse(
           403,
           pht('You do not have permission to access this repository.'));
       } else {
         if ($allow_auth) {
           return new PhabricatorVCSResponse(
             401,
             pht('You must log in to access this repository.'));
         } else {
           return new PhabricatorVCSResponse(
             403,
             pht(
               'This repository requires authentication, which is forbidden '.
               'over HTTP.'));
         }
       }
     }
 
     $response = $this->validateGitLFSRequest($repository, $viewer);
     if ($response) {
       return $response;
     }
 
     $this->setServiceRepository($repository);
 
     if (!$repository->isTracked()) {
       return new PhabricatorVCSResponse(
         403,
         pht('This repository is inactive.'));
     }
 
     $is_push = !$this->isReadOnlyRequest($repository);
 
     if ($this->getIsGitLFSRequest() && $this->getGitLFSToken()) {
       // We allow git LFS requests over HTTP even if the repository does not
       // otherwise support HTTP reads or writes, as long as the user is using a
       // token from SSH. If they're using HTTP username + password auth, they
       // have to obey the normal HTTP rules.
     } else {
       switch ($repository->getServeOverHTTP()) {
         case PhabricatorRepository::SERVE_READONLY:
           if ($is_push) {
             return new PhabricatorVCSResponse(
               403,
               pht('This repository is read-only over HTTP.'));
           }
           break;
         case PhabricatorRepository::SERVE_READWRITE:
           // We'll check for push capability below.
           break;
         case PhabricatorRepository::SERVE_OFF:
         default:
           return new PhabricatorVCSResponse(
             403,
             pht('This repository is not available over HTTP.'));
       }
     }
 
     if ($is_push) {
       $can_push = PhabricatorPolicyFilter::hasCapability(
         $viewer,
         $repository,
         DiffusionPushCapability::CAPABILITY);
       if (!$can_push) {
         if ($viewer->isLoggedIn()) {
-          return new PhabricatorVCSResponse(
-            403,
-            pht(
-              'You do not have permission to push to this '.
-              'repository.'));
+          $error_code = 403;
+          $error_message = pht(
+            'You do not have permission to push to this repository ("%s").',
+            $repository->getDisplayName());
+
+          if ($this->getIsGitLFSRequest()) {
+            return DiffusionGitLFSResponse::newErrorResponse(
+              $error_code,
+              $error_message);
+          } else {
+            return new PhabricatorVCSResponse(
+              $error_code,
+              $error_message);
+          }
         } else {
           if ($allow_auth) {
             return new PhabricatorVCSResponse(
               401,
               pht('You must log in to push to this repository.'));
           } else {
             return new PhabricatorVCSResponse(
               403,
               pht(
                 'Pushing to this repository requires authentication, '.
                 'which is forbidden over HTTP.'));
           }
         }
       }
     }
 
     $vcs_type = $repository->getVersionControlSystem();
     $req_type = $this->isVCSRequest($request);
 
     if ($vcs_type != $req_type) {
       switch ($req_type) {
         case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
           $result = new PhabricatorVCSResponse(
             500,
             pht(
               'This repository ("%s") is not a Git repository.',
               $repository->getDisplayName()));
           break;
         case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
           $result = new PhabricatorVCSResponse(
             500,
             pht(
               'This repository ("%s") is not a Mercurial repository.',
               $repository->getDisplayName()));
           break;
         case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
           $result = new PhabricatorVCSResponse(
             500,
             pht(
               'This repository ("%s") is not a Subversion repository.',
               $repository->getDisplayName()));
           break;
         default:
           $result = new PhabricatorVCSResponse(
             500,
             pht('Unknown request type.'));
           break;
       }
     } else {
       switch ($vcs_type) {
         case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
           $result = $this->serveVCSRequest($repository, $viewer);
           break;
         case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
           $result = new PhabricatorVCSResponse(
             500,
             pht(
               'Phabricator does not support HTTP access to Subversion '.
               'repositories.'));
           break;
         default:
           $result = new PhabricatorVCSResponse(
             500,
             pht('Unknown version control system.'));
           break;
       }
     }
 
     $code = $result->getHTTPResponseCode();
 
     if ($is_push && ($code == 200)) {
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
         $repository->writeStatusMessage(
           PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
           PhabricatorRepositoryStatusMessage::CODE_OKAY);
       unset($unguarded);
     }
 
     return $result;
   }
 
   private function serveVCSRequest(
     PhabricatorRepository $repository,
     PhabricatorUser $viewer) {
 
     // We can serve Git LFS requests first, since we don't need to proxy them.
     // It's also important that LFS requests never fall through to standard
     // service pathways, because that would let you use LFS tokens to read
     // normal repository data.
     if ($this->getIsGitLFSRequest()) {
       return $this->serveGitLFSRequest($repository, $viewer);
     }
 
     // If this repository is hosted on a service, we need to proxy the request
     // to a host which can serve it.
     $is_cluster_request = $this->getRequest()->isProxiedClusterRequest();
 
     $uri = $repository->getAlmanacServiceURI(
       $viewer,
       $is_cluster_request,
       array(
         'http',
         'https',
       ));
     if ($uri) {
       $future = $this->getRequest()->newClusterProxyFuture($uri);
       return id(new AphrontHTTPProxyResponse())
         ->setHTTPFuture($future);
     }
 
     // Otherwise, we're going to handle the request locally.
 
     $vcs_type = $repository->getVersionControlSystem();
     switch ($vcs_type) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         $result = $this->serveGitRequest($repository, $viewer);
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $result = $this->serveMercurialRequest($repository, $viewer);
         break;
     }
 
     return $result;
   }
 
   private function isReadOnlyRequest(
     PhabricatorRepository $repository) {
     $request = $this->getRequest();
     $method = $_SERVER['REQUEST_METHOD'];
 
     // TODO: This implementation is safe by default, but very incomplete.
 
-    // TODO: This doesn't get the right result for Git LFS yet.
+    if ($this->getIsGitLFSRequest()) {
+      return $this->isGitLFSReadOnlyRequest($repository);
+    }
 
     switch ($repository->getVersionControlSystem()) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         $service = $request->getStr('service');
         $path = $this->getRequestDirectoryPath($repository);
         // NOTE: Service names are the reverse of what you might expect, as they
         // are from the point of view of the server. The main read service is
         // "git-upload-pack", and the main write service is "git-receive-pack".
 
         if ($method == 'GET' &&
             $path == '/info/refs' &&
             $service == 'git-upload-pack') {
           return true;
         }
 
         if ($path == '/git-upload-pack') {
           return true;
         }
 
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $cmd = $request->getStr('cmd');
         if ($cmd == 'batch') {
           $cmds = idx($this->getMercurialArguments(), 'cmds');
           return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds);
         }
         return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd);
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         break;
     }
 
     return false;
   }
 
   /**
    * @phutil-external-symbol class PhabricatorStartup
    */
   private function serveGitRequest(
     PhabricatorRepository $repository,
     PhabricatorUser $viewer) {
     $request = $this->getRequest();
 
     $request_path = $this->getRequestDirectoryPath($repository);
     $repository_root = $repository->getLocalPath();
 
     // Rebuild the query string to strip `__magic__` parameters and prevent
     // issues where we might interpret inputs like "service=read&service=write"
     // differently than the server does and pass it an unsafe command.
 
     // NOTE: This does not use getPassthroughRequestParameters() because
     // that code is HTTP-method agnostic and will encode POST data.
 
     $query_data = $_GET;
     foreach ($query_data as $key => $value) {
       if (!strncmp($key, '__', 2)) {
         unset($query_data[$key]);
       }
     }
     $query_string = http_build_query($query_data, '', '&');
 
     // We're about to wipe out PATH with the rest of the environment, so
     // resolve the binary first.
     $bin = Filesystem::resolveBinary('git-http-backend');
     if (!$bin) {
       throw new Exception(
         pht(
           'Unable to find `%s` in %s!',
           'git-http-backend',
           '$PATH'));
     }
 
     // NOTE: We do not set HTTP_CONTENT_ENCODING here, because we already
     // decompressed the request when we read the request body, so the body is
     // just plain data with no encoding.
 
     $env = array(
       'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'],
       'QUERY_STRING' => $query_string,
       'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'),
       'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'],
       'GIT_PROJECT_ROOT' => $repository_root,
       'GIT_HTTP_EXPORT_ALL' => '1',
       'PATH_INFO' => $request_path,
 
       'REMOTE_USER' => $viewer->getUsername(),
 
       // TODO: Set these correctly.
       // GIT_COMMITTER_NAME
       // GIT_COMMITTER_EMAIL
     ) + $this->getCommonEnvironment($viewer);
 
     $input = PhabricatorStartup::getRawInput();
 
     $command = csprintf('%s', $bin);
     $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
 
     list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
       ->setEnv($env, true)
       ->write($input)
       ->resolve();
 
     if ($err) {
       if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) {
         // Ignore the error if the response passes this special check for
         // validity.
         $err = 0;
       }
     }
 
     if ($err) {
       return new PhabricatorVCSResponse(
         500,
         pht(
           'Error %d: %s',
           $err,
           phutil_utf8ize($stderr)));
     }
 
     return id(new DiffusionGitResponse())->setGitData($stdout);
   }
 
   private function getRequestDirectoryPath(PhabricatorRepository $repository) {
     $request = $this->getRequest();
     $request_path = $request->getRequestURI()->getPath();
 
     $info = PhabricatorRepository::parseRepositoryServicePath($request_path);
     $base_path = $info['path'];
 
     // For Git repositories, strip an optional directory component if it
     // isn't the name of a known Git resource. This allows users to clone
     // repositories as "/diffusion/X/anything.git", for example.
     if ($repository->isGit()) {
       $known = array(
         'info',
         'git-upload-pack',
         'git-receive-pack',
       );
 
       foreach ($known as $key => $path) {
         $known[$key] = preg_quote($path, '@');
       }
 
       $known = implode('|', $known);
 
       if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) {
         $base_path = preg_replace('@^/([^/]+)@', '', $base_path);
       }
     }
 
     return $base_path;
   }
 
   private function authenticateGitLFSUser(
     $username,
     PhutilOpaqueEnvelope $password) {
 
     // Never accept these credentials for requests which aren't LFS requests.
     if (!$this->getIsGitLFSRequest()) {
       return null;
     }
 
     // If we have the wrong username, don't bother checking if the token
     // is right.
     if ($username !== DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME) {
       return null;
     }
 
     $lfs_pass = $password->openEnvelope();
     $lfs_hash = PhabricatorHash::digest($lfs_pass);
 
     $token = id(new PhabricatorAuthTemporaryTokenQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE))
       ->withTokenCodes(array($lfs_hash))
       ->withExpired(false)
       ->executeOne();
     if (!$token) {
       return null;
     }
 
     $user = id(new PhabricatorPeopleQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withPHIDs(array($token->getUserPHID()))
       ->executeOne();
 
     if (!$user) {
       return null;
     }
 
     if (!$user->isUserActivated()) {
       return null;
     }
 
     $this->gitLFSToken = $token;
 
     return $user;
   }
 
   private function authenticateHTTPRepositoryUser(
     $username,
     PhutilOpaqueEnvelope $password) {
 
     if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) {
       // No HTTP auth permitted.
       return null;
     }
 
     if (!strlen($username)) {
       // No username.
       return null;
     }
 
     if (!strlen($password->openEnvelope())) {
       // No password.
       return null;
     }
 
     $user = id(new PhabricatorPeopleQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withUsernames(array($username))
       ->executeOne();
     if (!$user) {
       // Username doesn't match anything.
       return null;
     }
 
     if (!$user->isUserActivated()) {
       // User is not activated.
       return null;
     }
 
     $password_entry = id(new PhabricatorRepositoryVCSPassword())
       ->loadOneWhere('userPHID = %s', $user->getPHID());
     if (!$password_entry) {
       // User doesn't have a password set.
       return null;
     }
 
     if (!$password_entry->comparePassword($password, $user)) {
       // Password doesn't match.
       return null;
     }
 
     // If the user's password is stored using a less-than-optimal hash, upgrade
     // them to the strongest available hash.
 
     $hash_envelope = new PhutilOpaqueEnvelope(
       $password_entry->getPasswordHash());
     if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) {
       $password_entry->setPassword($password, $user);
       $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
         $password_entry->save();
       unset($unguarded);
     }
 
     return $user;
   }
 
   private function serveMercurialRequest(
     PhabricatorRepository $repository,
     PhabricatorUser $viewer) {
     $request = $this->getRequest();
 
     $bin = Filesystem::resolveBinary('hg');
     if (!$bin) {
       throw new Exception(
         pht(
           'Unable to find `%s` in %s!',
           'hg',
           '$PATH'));
     }
 
     $env = $this->getCommonEnvironment($viewer);
     $input = PhabricatorStartup::getRawInput();
 
     $cmd = $request->getStr('cmd');
 
     $args = $this->getMercurialArguments();
     $args = $this->formatMercurialArguments($cmd, $args);
 
     if (strlen($input)) {
       $input = strlen($input)."\n".$input."0\n";
     }
 
     $command = csprintf('%s serve --stdio', $bin);
     $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
 
     list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
       ->setEnv($env, true)
       ->setCWD($repository->getLocalPath())
       ->write("{$cmd}\n{$args}{$input}")
       ->resolve();
 
     if ($err) {
       return new PhabricatorVCSResponse(
         500,
         pht('Error %d: %s', $err, $stderr));
     }
 
     if ($cmd == 'getbundle' ||
         $cmd == 'changegroup' ||
         $cmd == 'changegroupsubset') {
       // We're not completely sure that "changegroup" and "changegroupsubset"
       // actually work, they're for very old Mercurial.
       $body = gzcompress($stdout);
     } else if ($cmd == 'unbundle') {
       // This includes diagnostic information and anything echoed by commit
       // hooks. We ignore `stdout` since it just has protocol garbage, and
       // substitute `stderr`.
       $body = strlen($stderr)."\n".$stderr;
     } else {
       list($length, $body) = explode("\n", $stdout, 2);
       if ($cmd == 'capabilities') {
         $body = DiffusionMercurialWireProtocol::filterBundle2Capability($body);
       }
     }
 
     return id(new DiffusionMercurialResponse())->setContent($body);
   }
 
   private function getMercurialArguments() {
     // Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
     // "Why would you do this?".
 
     $args_raw = array();
     for ($ii = 1;; $ii++) {
       $header = 'HTTP_X_HGARG_'.$ii;
       if (!array_key_exists($header, $_SERVER)) {
         break;
       }
       $args_raw[] = $_SERVER[$header];
     }
     $args_raw = implode('', $args_raw);
 
     return id(new PhutilQueryStringParser())
       ->parseQueryString($args_raw);
   }
 
   private function formatMercurialArguments($command, array $arguments) {
     $spec = DiffusionMercurialWireProtocol::getCommandArgs($command);
 
     $out = array();
 
     // Mercurial takes normal arguments like this:
     //
     //   name <length(value)>
     //   value
 
     $has_star = false;
     foreach ($spec as $arg_key) {
       if ($arg_key == '*') {
         $has_star = true;
         continue;
       }
       if (isset($arguments[$arg_key])) {
         $value = $arguments[$arg_key];
         $size = strlen($value);
         $out[] = "{$arg_key} {$size}\n{$value}";
         unset($arguments[$arg_key]);
       }
     }
 
     if ($has_star) {
 
       // Mercurial takes arguments for variable argument lists roughly like
       // this:
       //
       //   * <count(args)>
       //   argname1 <length(argvalue1)>
       //   argvalue1
       //   argname2 <length(argvalue2)>
       //   argvalue2
 
       $count = count($arguments);
 
       $out[] = "* {$count}\n";
 
       foreach ($arguments as $key => $value) {
         if (in_array($key, $spec)) {
           // We already added this argument above, so skip it.
           continue;
         }
         $size = strlen($value);
         $out[] = "{$key} {$size}\n{$value}";
       }
     }
 
     return implode('', $out);
   }
 
   private function isValidGitShallowCloneResponse($stdout, $stderr) {
     // If you execute `git clone --depth N ...`, git sends a request which
     // `git-http-backend` responds to by emitting valid output and then exiting
     // with a failure code and an error message. If we ignore this error,
     // everything works.
 
     // This is a pretty funky fix: it would be nice to more precisely detect
     // that a request is a `--depth N` clone request, but we don't have any code
     // to decode protocol frames yet. Instead, look for reasonable evidence
     // in the error and output that we're looking at a `--depth` clone.
 
     // For evidence this isn't completely crazy, see:
     // https://github.com/schacon/grack/pull/7
 
     $stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m';
     $stderr_regexp = '(The remote end hung up unexpectedly)';
 
     $has_pack = preg_match($stdout_regexp, $stdout);
     $is_hangup = preg_match($stderr_regexp, $stderr);
 
     return $has_pack && $is_hangup;
   }
 
   private function getCommonEnvironment(PhabricatorUser $viewer) {
     $remote_address = $this->getRequest()->getRemoteAddress();
 
     return array(
       DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(),
       DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address,
       DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http',
     );
   }
 
   private function validateGitLFSRequest(
     PhabricatorRepository $repository,
     PhabricatorUser $viewer) {
     if (!$this->getIsGitLFSRequest()) {
       return null;
     }
 
     if (!$repository->canUseGitLFS()) {
       return new PhabricatorVCSResponse(
         403,
         pht(
           'The requested repository ("%s") does not support Git LFS.',
           $repository->getDisplayName()));
     }
 
     // If this is using an LFS token, sanity check that we're using it on the
     // correct repository. This shouldn't really matter since the user could
     // just request a proper token anyway, but it suspicious and should not
     // be permitted.
 
     $token = $this->getGitLFSToken();
     if ($token) {
       $resource = $token->getTokenResource();
       if ($resource !== $repository->getPHID()) {
         return new PhabricatorVCSResponse(
           403,
           pht(
             'The authentication token provided in the request is bound to '.
             'a different repository than the requested repository ("%s").',
             $repository->getDisplayName()));
       }
     }
 
     return null;
   }
 
   private function serveGitLFSRequest(
     PhabricatorRepository $repository,
     PhabricatorUser $viewer) {
 
     if (!$this->getIsGitLFSRequest()) {
       throw new Exception(pht('This is not a Git LFS request!'));
     }
 
     $path = $this->getGitLFSRequestPath($repository);
     $matches = null;
 
     if (preg_match('(^upload/(.*)\z)', $path, $matches)) {
       $oid = $matches[1];
       return $this->serveGitLFSUploadRequest($repository, $viewer, $oid);
     } else if ($path == 'objects/batch') {
       return $this->serveGitLFSBatchRequest($repository, $viewer);
     } else {
       return DiffusionGitLFSResponse::newErrorResponse(
         404,
         pht(
           'Git LFS operation "%s" is not supported by this server.',
           $path));
     }
   }
 
   private function serveGitLFSBatchRequest(
     PhabricatorRepository $repository,
     PhabricatorUser $viewer) {
 
-    $input = PhabricatorStartup::getRawInput();
-    $input = phutil_json_decode($input);
+    $input = $this->getGitLFSInput();
 
     $operation = idx($input, 'operation');
     switch ($operation) {
       case 'upload':
         $want_upload = true;
         break;
       case 'download':
         $want_upload = false;
         break;
       default:
         return DiffusionGitLFSResponse::newErrorResponse(
           404,
           pht(
             'Git LFS batch operation "%s" is not supported by this server.',
             $operation));
     }
 
     $objects = idx($input, 'objects', array());
 
     $hashes = array();
     foreach ($objects as $object) {
       $hashes[] = idx($object, 'oid');
     }
 
     if ($hashes) {
       $refs = id(new PhabricatorRepositoryGitLFSRefQuery())
         ->setViewer($viewer)
         ->withRepositoryPHIDs(array($repository->getPHID()))
         ->withObjectHashes($hashes)
         ->execute();
       $refs = mpull($refs, null, 'getObjectHash');
     } else {
       $refs = array();
     }
 
     $file_phids = mpull($refs, 'getFilePHID');
     if ($file_phids) {
       $files = id(new PhabricatorFileQuery())
         ->setViewer($viewer)
         ->withPHIDs($file_phids)
         ->execute();
       $files = mpull($files, null, 'getPHID');
     } else {
       $files = array();
     }
 
     $authorization = null;
     $output = array();
     foreach ($objects as $object) {
       $oid = idx($object, 'oid');
       $size = idx($object, 'size');
       $ref = idx($refs, $oid);
       $error = null;
 
       // NOTE: If we already have a ref for this object, we only emit a
       // "download" action. The client should not upload the file again.
 
       $actions = array();
       if ($ref) {
         $file = idx($files, $ref->getFilePHID());
         if ($file) {
           // Git LFS may prompt users for authentication if the action does
           // not provide an "Authorization" header and does not have a query
           // parameter named "token". See here for discussion:
           // <https://github.com/github/git-lfs/issues/1088>
           $no_authorization = 'Basic '.base64_encode('none');
 
           $get_uri = $file->getCDNURIWithToken();
           $actions['download'] = array(
             'href' => $get_uri,
             'header' => array(
               'Authorization' => $no_authorization,
             ),
           );
         } else {
           $error = array(
             'code' => 404,
             'message' => pht(
               'Object "%s" was previously uploaded, but no longer exists '.
               'on this server.',
               $oid),
           );
         }
       } else if ($want_upload) {
         if (!$authorization) {
           // Here, we could reuse the existing authorization if we have one,
           // but it's a little simpler to just generate a new one
           // unconditionally.
           $authorization = $this->newGitLFSHTTPAuthorization(
             $repository,
             $viewer,
             $operation);
         }
 
         $put_uri = $repository->getGitLFSURI("info/lfs/upload/{$oid}");
 
         $actions['upload'] = array(
           'href' => $put_uri,
           'header' => array(
             'Authorization' => $authorization,
             'X-Phabricator-Request-Type' => 'git-lfs',
           ),
         );
       }
 
       $object = array(
         'oid' => $oid,
         'size' => $size,
       );
 
       if ($actions) {
         $object['actions'] = $actions;
       }
 
       if ($error) {
         $object['error'] = $error;
       }
 
       $output[] = $object;
     }
 
     $output = array(
       'objects' => $output,
     );
 
     return id(new DiffusionGitLFSResponse())
       ->setContent($output);
   }
 
   private function serveGitLFSUploadRequest(
     PhabricatorRepository $repository,
     PhabricatorUser $viewer,
     $oid) {
 
     $ref = id(new PhabricatorRepositoryGitLFSRefQuery())
       ->setViewer($viewer)
       ->withRepositoryPHIDs(array($repository->getPHID()))
       ->withObjectHashes(array($oid))
       ->executeOne();
     if ($ref) {
       return DiffusionGitLFSResponse::newErrorResponse(
         405,
         pht(
           'Content for object "%s" is already known to this server. It can '.
           'not be uploaded again.',
           $oid));
     }
 
+    // Remove the execution time limit because uploading large files may take
+    // a while.
+    set_time_limit(0);
+
     $request_stream = new AphrontRequestStream();
     $request_iterator = $request_stream->getIterator();
     $hashing_iterator = id(new PhutilHashingIterator($request_iterator))
       ->setAlgorithm('sha256');
 
     $source = id(new PhabricatorIteratorFileUploadSource())
       ->setName('lfs-'.$oid)
       ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
       ->setIterator($hashing_iterator);
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
       $file = $source->uploadFile();
     unset($unguarded);
 
     $hash = $hashing_iterator->getHash();
     if ($hash !== $oid) {
       return DiffusionGitLFSResponse::newErrorResponse(
         400,
         pht(
           'Uploaded data is corrupt or invalid. Expected hash "%s", actual '.
           'hash "%s".',
           $oid,
           $hash));
     }
 
     $ref = id(new PhabricatorRepositoryGitLFSRef())
       ->setRepositoryPHID($repository->getPHID())
       ->setObjectHash($hash)
       ->setByteSize($file->getByteSize())
       ->setAuthorPHID($viewer->getPHID())
       ->setFilePHID($file->getPHID());
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
       // Attach the file to the repository to give users permission
       // to access it.
       $file->attachToObject($repository->getPHID());
       $ref->save();
     unset($unguarded);
 
     // This is just a plain HTTP 200 with no content, which is what `git lfs`
     // expects.
     return new DiffusionGitLFSResponse();
   }
 
   private function newGitLFSHTTPAuthorization(
     PhabricatorRepository $repository,
     PhabricatorUser $viewer,
     $operation) {
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
     $authorization = DiffusionGitLFSTemporaryTokenType::newHTTPAuthorization(
       $repository,
       $viewer,
       $operation);
 
     unset($unguarded);
 
     return $authorization;
   }
 
   private function getGitLFSRequestPath(PhabricatorRepository $repository) {
     $request_path = $this->getRequestDirectoryPath($repository);
 
     $matches = null;
     if (preg_match('(^/info/lfs(?:\z|/)(.*))', $request_path, $matches)) {
       return $matches[1];
     }
 
     return null;
   }
 
+  private function getGitLFSInput() {
+    if (!$this->gitLFSInput) {
+      $input = PhabricatorStartup::getRawInput();
+      $input = phutil_json_decode($input);
+      $this->gitLFSInput = $input;
+    }
+
+    return $this->gitLFSInput;
+  }
+
+  private function isGitLFSReadOnlyRequest(PhabricatorRepository $repository) {
+    if (!$this->getIsGitLFSRequest()) {
+      return false;
+    }
+
+    $path = $this->getGitLFSRequestPath($repository);
+
+    if ($path === 'objects/batch') {
+      $input = $this->getGitLFSInput();
+      $operation = idx($input, 'operation');
+      switch ($operation) {
+        case 'download':
+          return true;
+        default:
+          return false;
+      }
+    }
+
+    return false;
+  }
+
+
 }