1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-28 17:52:43 +01:00
phorge-phorge/src/applications/diffusion/DiffusionLintSaveRunner.php
epriestley a74545c9da Provide a rough, unstable API for reporting coverage into Diffusion
Summary:
Ref T4994. This stuff works:

  - You can dump a blob of coverage information into `diffusion.updatecoverage`. This wipes existing coverage information and replaces it.
  - It shows up when viewing files.
  - It shows up when viewing commits.

This stuff does not work:

  - When viewing files, the Javascript hover interaction isn't tied in yet.
  - We always show this information, even if you're behind the commit where it was generated.
  - You can't do incremental updates.
  - There's no aggregation at the file (this file has 90% coverage), diff (the changes in this commit are 90% covered), or directory (the code in this directory has 90% coverage) levels yet.
  - This is probably not the final form of the UI, storage, or API, so you should expect occasional changes over time. I've marked the method as "Unstable" for now.

Test Plan:
  - Ran `save_lint.php` to check for collateral damage; it worked fine.
  - Ran `save_lint.php` on a new branch to check creation.
  - Published some fake coverage information.
  - Viewed an affected commit.
  - Viewed an affected file.

{F151915}

{F151916}

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: jhurwitz, epriestley, zeeg

Maniphest Tasks: T5044, T4994

Differential Revision: https://secure.phabricator.com/D9022
2014-05-17 16:10:54 -07:00

289 lines
7.6 KiB
PHP

<?php
final class DiffusionLintSaveRunner {
private $arc = 'arc';
private $severity = ArcanistLintSeverity::SEVERITY_ADVICE;
private $all = false;
private $chunkSize = 256;
private $needsBlame = false;
private $svnRoot;
private $lintCommit;
private $branch;
private $conn;
private $deletes = array();
private $inserts = array();
private $blame = array();
public function setArc($path) {
$this->arc = $path;
return $this;
}
public function setSeverity($string) {
$this->severity = $string;
return $this;
}
public function setAll($bool) {
$this->all = $bool;
return $this;
}
public function setChunkSize($number) {
$this->chunkSize = $number;
return $this;
}
public function setNeedsBlame($boolean) {
$this->needsBlame = $boolean;
return $this;
}
public function run($dir) {
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($dir);
$configuration_manager = new ArcanistConfigurationManager();
$configuration_manager->setWorkingCopyIdentity($working_copy);
$api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
$configuration_manager);
$this->svnRoot = id(new PhutilURI($api->getSourceControlPath()))->getPath();
if ($api instanceof ArcanistGitAPI) {
$svn_fetch = $api->getGitConfig('svn-remote.svn.fetch');
list($this->svnRoot) = explode(':', $svn_fetch);
if ($this->svnRoot != '') {
$this->svnRoot = '/'.$this->svnRoot;
}
}
$project_id = $working_copy->getProjectID();
$project = id(new PhabricatorRepositoryArcanistProject())
->loadOneWhere('name = %s', $project_id);
if (!$project || !$project->getRepositoryID()) {
throw new Exception("Couldn't find repository for {$project_id}.");
}
$branch_name = $api->getBranchName();
$this->branch = PhabricatorRepositoryBranch::loadOrCreateBranch(
$project->getRepositoryID(),
$branch_name);
$this->conn = $this->branch->establishConnection('w');
$this->lintCommit = null;
if (!$this->all) {
$this->lintCommit = $this->branch->getLintCommit();
}
if ($this->lintCommit) {
try {
$commit = $this->lintCommit;
if ($this->svnRoot) {
$commit = $api->getCanonicalRevisionName('@'.$commit);
}
$all_files = $api->getChangedFiles($commit);
} catch (ArcanistCapabilityNotSupportedException $ex) {
$this->lintCommit = null;
}
}
if (!$this->lintCommit) {
$where = ($this->svnRoot
? qsprintf($this->conn, 'AND path LIKE %>', $this->svnRoot.'/')
: '');
queryfx(
$this->conn,
'DELETE FROM %T WHERE branchID = %d %Q',
PhabricatorRepository::TABLE_LINTMESSAGE,
$this->branch->getID(),
$where);
$all_files = $api->getAllFiles();
}
$count = 0;
$files = array();
foreach ($all_files as $file => $val) {
$count++;
if (!$this->lintCommit) {
$file = $val;
} else {
$this->deletes[] = $this->svnRoot.'/'.$file;
if ($val & ArcanistRepositoryAPI::FLAG_DELETED) {
continue;
}
}
$files[$file] = $file;
if (count($files) >= $this->chunkSize) {
$this->runArcLint($files);
$files = array();
}
}
$this->runArcLint($files);
$this->saveLintMessages();
$this->lintCommit = $api->getUnderlyingWorkingCopyRevision();
$this->branch->setLintCommit($this->lintCommit);
$this->branch->save();
if ($this->blame) {
$this->blameAuthors();
$this->blame = array();
}
return $count;
}
private function runArcLint(array $files) {
if (!$files) {
return;
}
echo '.';
try {
$future = new ExecFuture(
'%C lint --severity %s --output json %Ls',
$this->arc,
$this->severity,
$files);
foreach (new LinesOfALargeExecFuture($future) as $json) {
$paths = json_decode($json, true);
if (!is_array($paths)) {
fprintf(STDERR, "Invalid JSON: {$json}\n");
continue;
}
foreach ($paths as $path => $messages) {
if (!isset($files[$path])) {
continue;
}
foreach ($messages as $message) {
$line = idx($message, 'line', 0);
$this->inserts[] = qsprintf(
$this->conn,
'(%d, %s, %d, %s, %s, %s, %s)',
$this->branch->getID(),
$this->svnRoot.'/'.$path,
$line,
idx($message, 'code', ''),
idx($message, 'severity', ''),
idx($message, 'name', ''),
idx($message, 'description', ''));
if ($line && $this->needsBlame) {
$this->blame[$path][$line] = true;
}
}
if (count($this->deletes) >= 1024 || count($this->inserts) >= 256) {
$this->saveLintMessages();
}
}
}
} catch (Exception $ex) {
fprintf(STDERR, $ex->getMessage()."\n");
}
}
private function saveLintMessages() {
$this->conn->openTransaction();
foreach (array_chunk($this->deletes, 1024) as $paths) {
queryfx(
$this->conn,
'DELETE FROM %T WHERE branchID = %d AND path IN (%Ls)',
PhabricatorRepository::TABLE_LINTMESSAGE,
$this->branch->getID(),
$paths);
}
foreach (array_chunk($this->inserts, 256) as $values) {
queryfx(
$this->conn,
'INSERT INTO %T
(branchID, path, line, code, severity, name, description)
VALUES %Q',
PhabricatorRepository::TABLE_LINTMESSAGE,
implode(', ', $values));
}
$this->conn->saveTransaction();
$this->deletes = array();
$this->inserts = array();
}
private function blameAuthors() {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($this->branch->getRepositoryID()))
->executeOne();
$queries = array();
$futures = array();
foreach ($this->blame as $path => $lines) {
$drequest = DiffusionRequest::newFromDictionary(array(
'user' => PhabricatorUser::getOmnipotentUser(),
'initFromConduit' => false,
'repository' => $repository,
'branch' => $this->branch->getName(),
'path' => $path,
'commit' => $this->lintCommit,
));
$query = DiffusionFileContentQuery::newFromDiffusionRequest($drequest)
->setNeedsBlame(true);
$queries[$path] = $query;
$futures[$path] = $query->getFileContentFuture();
}
$authors = array();
foreach (Futures($futures)->limit(8) as $path => $future) {
$queries[$path]->loadFileContentFromFuture($future);
list(, $rev_list, $blame_dict) = $queries[$path]->getBlameData();
foreach (array_keys($this->blame[$path]) as $line) {
$commit_identifier = $rev_list[$line - 1];
$author = idx($blame_dict[$commit_identifier], 'authorPHID');
if ($author) {
$authors[$author][$path][] = $line;
}
}
}
if ($authors) {
$this->conn->openTransaction();
foreach ($authors as $author => $paths) {
$where = array();
foreach ($paths as $path => $lines) {
$where[] = qsprintf(
$this->conn,
'(path = %s AND line IN (%Ld))',
$this->svnRoot.'/'.$path,
$lines);
}
queryfx(
$this->conn,
'UPDATE %T SET authorPHID = %s WHERE %Q',
PhabricatorRepository::TABLE_LINTMESSAGE,
$author,
implode(' OR ', $where));
}
$this->conn->saveTransaction();
}
}
}