diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -164,6 +164,16 @@ $this->applyHeraldRefRules($ref_updates); } + try { + if (!$is_initial_import) { + $this->rejectOversizedFiles($content_updates); + } + } catch (DiffusionCommitHookRejectException $ex) { + // If we're rejecting oversized files, flag everything. + $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_OVERSIZED; + throw $ex; + } + try { if (!$is_initial_import) { $this->rejectEnormousChanges($content_updates); @@ -1255,6 +1265,139 @@ return $changesets; } + private function rejectOversizedFiles(array $content_updates) { + $repository = $this->getRepository(); + + // TODO: Allow repositories to be configured for a maximum filesize. + $limit = 0; + + if (!$limit) { + return; + } + + foreach ($content_updates as $update) { + $identifier = $update->getRefNew(); + + $sizes = $this->loadFileSizesForCommit($identifier); + foreach ($sizes as $path => $size) { + if ($size <= $limit) { + continue; + } + + $message = pht( + 'OVERSIZED FILE'. + "\n". + 'This repository ("%s") is configured with a maximum individual '. + 'file size limit, but you are pushing a change ("%s") which causes '. + 'the size of a file ("%s") to exceed the limit. The commit makes '. + 'the file %s bytes long, but the limit for this repository is '. + '%s bytes.', + $repository->getDisplayName(), + $identifier, + $path, + new PhutilNumber($size), + new PhutilNumber($limit)); + + throw new DiffusionCommitHookRejectException($message); + } + } + } + + public function loadFileSizesForCommit($identifier) { + $repository = $this->getRepository(); + $vcs = $repository->getVersionControlSystem(); + + $path_sizes = array(); + + switch ($vcs) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + list($paths_raw) = $repository->execxLocalCommand( + 'diff-tree -z -r --no-commit-id %s --', + $identifier); + + // With "-z" we get "\0\0" for each line. Group the + // delimited text into ", " pairs. + $paths_raw = trim($paths_raw, "\0"); + $paths_raw = explode("\0", $paths_raw); + if (count($paths_raw) % 2) { + throw new Exception( + pht( + 'Unexpected number of output lines from "git diff-tree" when '. + 'processing commit ("%s"): got %s lines, expected an even '. + 'number.', + $identifier, + phutil_count($paths_raw))); + } + $paths_raw = array_chunk($paths_raw, 2); + + $paths = array(); + foreach ($paths_raw as $path_raw) { + list($fields, $pathname) = $path_raw; + $fields = explode(' ', $fields); + + // Fields are: + // + // :100644 100644 aaaa bbbb M + // + // [0] Old file mode. + // [1] New file mode. + // [2] Old object hash. + // [3] New object hash. + // [4] Change mode. + + $paths[] = array( + 'path' => $pathname, + 'newHash' => $fields[3], + ); + } + + if ($paths) { + $check_paths = array(); + foreach ($paths as $path) { + if ($path['newHash'] === self::EMPTY_HASH) { + $path_sizes[$path['path']] = 0; + continue; + } + $check_paths[$path['newHash']][] = $path['path']; + } + + if ($check_paths) { + $future = $repository->getLocalCommandFuture( + 'cat-file --batch-check=%s', + '%(objectsize)') + ->write(implode("\n", array_keys($check_paths))); + + list($sizes) = $future->resolvex(); + $sizes = trim($sizes); + $sizes = phutil_split_lines($sizes, false); + if (count($sizes) !== count($check_paths)) { + throw new Exception( + pht( + 'Unexpected number of output lines from "git cat-file" when '. + 'processing commit ("%s"): got %s lines, expected %s.', + $identifier, + phutil_count($sizes), + phutil_count($check_paths))); + } + + foreach ($check_paths as $object_hash => $path_names) { + $object_size = (int)array_shift($sizes); + foreach ($path_names as $path_name) { + $path_sizes[$path_name] = $object_size; + } + } + } + } + break; + default: + throw new Exception( + pht( + 'File size limits are not supported for this VCS.')); + } + + return $path_sizes; + } + public function loadCommitRefForCommit($identifier) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php --- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php @@ -24,6 +24,7 @@ const CHANGEFLAG_REWRITE = 8; const CHANGEFLAG_DANGEROUS = 16; const CHANGEFLAG_ENORMOUS = 32; + const CHANGEFLAG_OVERSIZED = 64; const REJECT_ACCEPT = 0; const REJECT_DANGEROUS = 1; @@ -31,6 +32,7 @@ const REJECT_EXTERNAL = 3; const REJECT_BROKEN = 4; const REJECT_ENORMOUS = 5; + const REJECT_OVERSIZED = 6; protected $repositoryPHID; protected $epoch; @@ -63,6 +65,7 @@ self::CHANGEFLAG_REWRITE => pht('Rewrite'), self::CHANGEFLAG_DANGEROUS => pht('Dangerous'), self::CHANGEFLAG_ENORMOUS => pht('Enormous'), + self::CHANGEFLAG_OVERSIZED => pht('Oversized'), ); } @@ -74,6 +77,7 @@ self::REJECT_EXTERNAL => pht('Rejected: External Hook'), self::REJECT_BROKEN => pht('Rejected: Broken'), self::REJECT_ENORMOUS => pht('Rejected: Enormous'), + self::REJECT_OVERSIZED => pht('Rejected: Oversized File'), ); }