diff --git a/resources/sql/patches/011.badcommit.sql b/resources/sql/patches/011.badcommit.sql new file mode 100644 index 0000000000..4ec76332b0 --- /dev/null +++ b/resources/sql/patches/011.badcommit.sql @@ -0,0 +1,44 @@ +CREATE DATABASE phabricator_herald; + +CREATE TABLE phabricator_herald.herald_action ( + id int unsigned not null auto_increment primary key, + ruleID int unsigned not null, + action varchar(255) not null, + target text not null +); + +CREATE TABLE phabricator_herald.herald_rule ( + id int unsigned not null auto_increment primary key, + name varchar(255) not null, + authorPHID varchar(64) binary not null, + contentType varchar(255) not null, + mustMatchAll bool not null, + configVersion int unsigned not null default '1', + dateCreated int unsigned not null, + dateModified int unsigned not null, + unique key (authorPHID, name) +); + +CREATE TABLE phabricator_herald.herald_condition ( + id int unsigned not null auto_increment primary key, + ruleID int unsigned not null, + fieldName varchar(255) not null, + fieldCondition varchar(255) not null, + value text not null +); + +CREATE TABLE phabricator_herald.herald_transcript ( + id int unsigned not null auto_increment primary key, + phid varchar(64) binary not null, + time int unsigned not null, + host varchar(255) not null, + psth varchar(255) not null, + duration float not null, + objectPHID varchar(64) binary not null, + dryRun bool not null, + objectTranscript longblob not null, + ruleTranscripts longblob not null, + conditionTranscripts longblob not null, + applyTranscripts longblob not null, + unique key (phid) +); \ No newline at end of file diff --git a/src/applications/diffusion/controller/commit/DiffusionCommitController.php b/src/applications/diffusion/controller/commit/DiffusionCommitController.php index 11a754c9ec..0c62de6e0a 100644 --- a/src/applications/diffusion/controller/commit/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/commit/DiffusionCommitController.php @@ -1,94 +1,119 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class DiffusionCommitController extends DiffusionController { public function processRequest() { $drequest = $this->getDiffusionRequest(); $content = array(); $content[] = $this->buildCrumbs(array( 'commit' => true, )); $detail_panel = new AphrontPanelView(); $repository = $drequest->getRepository(); $commit = $drequest->loadCommit(); + + if (!$commit) { + // TODO: Make more user-friendly. + throw new Exception('This commit has not parsed yet.'); + } + $commit_data = $drequest->loadCommitData(); require_celerity_resource('diffusion-commit-view-css'); $detail_panel->appendChild( '<div class="diffusion-commit-view">'. '<div class="diffusion-commit-dateline">'. 'r'.$repository->getCallsign().$commit->getCommitIdentifier(). ' · '. date('F jS, Y g:i A', $commit->getEpoch()). '</div>'. '<h1>Revision Detail</h1>'. '<div class="diffusion-commit-details">'. '<table class="diffusion-commit-properties">'. '<tr>'. '<th>Author:</th>'. '<td>'.phutil_escape_html($commit_data->getAuthorName()).'</td>'. '</tr>'. '</table>'. '<hr />'. '<div class="diffusion-commit-message">'. phutil_escape_html($commit_data->getCommitMessage()). '</div>'. '</div>'. '</div>'); $content[] = $detail_panel; $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $changes = $change_query->loadChanges(); $change_table = new DiffusionCommitChangeTableView(); $change_table->setDiffusionRequest($drequest); $change_table->setPathChanges($changes); // TODO: Large number of modified files check. $count = number_format(count($changes)); - $change_panel = new AphrontPanelView(); - $change_panel->setHeader("Changes ({$count})"); - $change_panel->appendChild($change_table); - - $content[] = $change_panel; - - - $change_list = - '<div style="margin: 2em; color: #666; padding: 1em; background: #eee;">'. - '(list of changes goes here)'. - '</div>'; - - $content[] = $change_list; + $bad_commit = null; + if ($count == 0) { + $bad_commit = queryfx_one( + id(new PhabricatorRepository())->establishConnection('r'), + 'SELECT * FROM %T WHERE fullCommitName = %s', + PhabricatorRepository::TABLE_BADCOMMIT, + 'r'.$repository->getCallsign().$commit->getCommitIdentifier()); + } + + if ($bad_commit) { + $error_panel = new AphrontErrorView(); + $error_panel->setWidth(AphrontErrorView::WIDTH_WIDE); + $error_panel->setTitle('Bad Commit'); + $error_panel->appendChild( + phutil_escape_html($bad_commit['description'])); + + $content[] = $error_panel; + } else { + $change_panel = new AphrontPanelView(); + $change_panel->setHeader("Changes ({$count})"); + $change_panel->appendChild($change_table); + + $content[] = $change_panel; + + $change_list = + '<div style="margin: 2em; color: #666; padding: 1em; + background: #eee;">'. + '(list of changes goes here)'. + '</div>'; + + $content[] = $change_list; + } return $this->buildStandardPageResponse( $content, array( 'title' => 'Diffusion', )); } } diff --git a/src/applications/diffusion/controller/commit/__init__.php b/src/applications/diffusion/controller/commit/__init__.php index 0abf62d921..6e629ab204 100644 --- a/src/applications/diffusion/controller/commit/__init__.php +++ b/src/applications/diffusion/controller/commit/__init__.php @@ -1,18 +1,22 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/diffusion/controller/base'); phutil_require_module('phabricator', 'applications/diffusion/query/pathchange/base'); phutil_require_module('phabricator', 'applications/diffusion/view/commitchangetable'); +phutil_require_module('phabricator', 'applications/repository/storage/repository'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); +phutil_require_module('phabricator', 'storage/queryfx'); +phutil_require_module('phabricator', 'view/form/error'); phutil_require_module('phabricator', 'view/layout/panel'); phutil_require_module('phutil', 'markup'); +phutil_require_module('phutil', 'utils'); phutil_require_source('DiffusionCommitController.php'); diff --git a/src/applications/repository/storage/repository/PhabricatorRepository.php b/src/applications/repository/storage/repository/PhabricatorRepository.php index 8212b4bff0..ec2e0f1059 100644 --- a/src/applications/repository/storage/repository/PhabricatorRepository.php +++ b/src/applications/repository/storage/repository/PhabricatorRepository.php @@ -1,56 +1,57 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class PhabricatorRepository extends PhabricatorRepositoryDAO { const TABLE_PATH = 'repository_path'; const TABLE_PATHCHANGE = 'repository_pathchange'; const TABLE_FILESYSTEM = 'repository_filesystem'; const TABLE_SUMMARY = 'repository_summary'; + const TABLE_BADCOMMIT = 'repository_badcommit'; protected $phid; protected $name; protected $callsign; protected $versionControlSystem; protected $details = array(); public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_REPO); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } } diff --git a/src/applications/repository/worker/base/PhabricatorRepositoryCommitParserWorker.php b/src/applications/repository/worker/base/PhabricatorRepositoryCommitParserWorker.php index 19346397b0..fc23edac0f 100644 --- a/src/applications/repository/worker/base/PhabricatorRepositoryCommitParserWorker.php +++ b/src/applications/repository/worker/base/PhabricatorRepositoryCommitParserWorker.php @@ -1,87 +1,99 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ abstract class PhabricatorRepositoryCommitParserWorker extends PhabricatorWorker { protected $commit; final public function doWork() { $commit_id = $this->getTaskData(); if (!$commit_id) { return; } $commit = id(new PhabricatorRepositoryCommit())->load($commit_id); if (!$commit) { // TODO: Communicate permanent failure? return; } $this->commit = $commit; $repository = id(new PhabricatorRepository())->load( $commit->getRepositoryID()); if (!$repository) { return; } return $this->parseCommit($repository, $commit); } abstract protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit); /** * This method is kind of awkward here but both the SVN message and * change parsers use it. */ protected function getSVNLogXMLObject($uri, $revision) { try { list($xml) = execx( 'svn log --xml --limit 1 --non-interactive %s@%d', $uri, $revision); } catch (CommandException $ex) { // HTTPS is generally faster and more reliable than svn+ssh, but some // commit messages with non-UTF8 text can't be retrieved over HTTPS, see // Facebook rE197184 for one example. Make an attempt to fall back to // svn+ssh if we've failed outright to retrieve the message. $fallback_uri = new PhutilURI($uri); if ($fallback_uri->getProtocol() != 'https') { throw $ex; } $fallback_uri->setProtocol('svn+ssh'); list($xml) = execx( 'svn log --xml --limit 1 --non-interactive %s@%d', $fallback_uri, $revision); } // Subversion may send us back commit messages which won't parse because // they have non UTF-8 garbage in them. Slam them into valid UTF-8. $xml = phutil_utf8ize($xml); return new SimpleXMLElement($xml); } + protected function isBadCommit($full_commit_name) { + $repository = new PhabricatorRepository(); + + $bad_commit = queryfx_one( + $repository->establishConnection('w'), + 'SELECT * FROM %T WHERE fullCommitName = %s', + PhabricatorRepository::TABLE_BADCOMMIT, + $full_commit_name); + + return (bool)$bad_commit; + } + } diff --git a/src/applications/repository/worker/base/__init__.php b/src/applications/repository/worker/base/__init__.php index ff7c96282d..d78aaa2839 100644 --- a/src/applications/repository/worker/base/__init__.php +++ b/src/applications/repository/worker/base/__init__.php @@ -1,18 +1,19 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/repository/storage/commit'); phutil_require_module('phabricator', 'applications/repository/storage/repository'); phutil_require_module('phabricator', 'infrastructure/daemon/workers/worker'); +phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phutil', 'future/exec'); phutil_require_module('phutil', 'parser/uri'); phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorRepositoryCommitParserWorker.php'); diff --git a/src/applications/repository/worker/commitchangeparser/git/PhabricatorRepositoryGitCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/git/PhabricatorRepositoryGitCommitChangeParserWorker.php index 8baa3d5f6d..b3d504f1a1 100644 --- a/src/applications/repository/worker/commitchangeparser/git/PhabricatorRepositoryGitCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/git/PhabricatorRepositoryGitCommitChangeParserWorker.php @@ -1,242 +1,249 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class PhabricatorRepositoryGitCommitChangeParserWorker extends PhabricatorRepositoryCommitChangeParserWorker { protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { + $full_name = 'r'.$repository->getCallsign().$commit->getCommitIdentifier(); + echo "Parsing {$full_name}...\n"; + if ($this->isBadCommit($full_name)) { + echo "This commit is marked bad!\n"; + return; + } + $local_path = $repository->getDetail('local-path'); list($raw) = execx( '(cd %s && git log -n1 -M -C -B --find-copies-harder --raw -t '. '--abbrev=40 --pretty=format: %s)', $local_path, $commit->getCommitIdentifier()); $changes = array(); $move_away = array(); $copy_away = array(); $lines = explode("\n", $raw); foreach ($lines as $line) { if (!strlen(trim($line))) { continue; } list($old_mode, $new_mode, $old_hash, $new_hash, $more_stuff) = preg_split('/ +/', $line); // We may only have two pieces here. list($action, $src_path, $dst_path) = array_merge( explode("\t", $more_stuff), array(null)); // Normalize the paths for consistency with the SVN workflow. $src_path = '/'.$src_path; if ($dst_path) { $dst_path = '/'.$dst_path; } $old_mode = intval($old_mode, 8); $new_mode = intval($new_mode, 8); $file_type = DifferentialChangeType::FILE_NORMAL; if ($new_mode & 040000) { $file_type = DifferentialChangeType::FILE_DIRECTORY; } else if ($new_mode & 0120000) { $file_type = DifferentialChangeType::FILE_SYMLINK; } // TODO: We can detect binary changes as git does, through a combination // of running 'git check-attr' for stuff like 'binary', 'merge' or 'diff', // and by falling back to inspecting the first 8,000 characters of the // buffer for null bytes (this is seriously git's algorithm, see // buffer_is_binary() in xdiff-interface.c). $change_type = null; $change_path = $src_path; $change_target = null; $is_direct = true; switch ($action[0]) { case 'A': $change_type = DifferentialChangeType::TYPE_ADD; break; case 'D': $change_type = DifferentialChangeType::TYPE_DELETE; break; case 'C': $change_type = DifferentialChangeType::TYPE_COPY_HERE; $change_path = $dst_path; $change_target = $src_path; $copy_away[$change_target][] = $change_path; break; case 'R': $change_type = DifferentialChangeType::TYPE_MOVE_HERE; $change_path = $dst_path; $change_target = $src_path; $move_away[$change_target][] = $change_path; break; case 'M': if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { $change_type = DifferentialChangeType::TYPE_CHILD; $is_direct = false; } else { $change_type = DifferentialChangeType::TYPE_CHANGE; } break; default: throw new Exception("Failed to parse line '{$line}'."); } $changes[$change_path] = array( 'repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $file_type, 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null, ); } // Add a change to '/' since git doesn't mention it. $changes['/'] = array( 'repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => '/', 'changeType' => DifferentialChangeType::TYPE_CHILD, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'isDirect' => false, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null, ); foreach ($copy_away as $change_path => $destinations) { if (isset($move_away[$change_path])) { $change_type = DifferentialChangeType::TYPE_MULTICOPY; $is_direct = true; unset($move_away[$change_path]); } else { $change_type = DifferentialChangeType::TYPE_COPY_AWAY; $is_direct = false; } $reference = $changes[reset($destinations)]; $changes[$change_path] = array( 'repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $reference['fileType'], 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null, ); } foreach ($move_away as $change_path => $destinations) { $reference = $changes[reset($destinations)]; $changes[$change_path] = array( 'repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => DifferentialChangeType::TYPE_MOVE_AWAY, 'fileType' => $reference['fileType'], 'isDirect' => true, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null, ); } $paths = array(); foreach ($changes as $change) { $paths[$change['path']] = true; if ($change['targetPath']) { $paths[$change['targetPath']] = true; } } $path_map = $this->lookupOrCreatePaths(array_keys($paths)); foreach ($changes as $key => $change) { $changes[$key]['pathID'] = $path_map[$change['path']]; if ($change['targetPath']) { $changes[$key]['targetPathID'] = $path_map[$change['targetPath']]; } else { $changes[$key]['targetPathID'] = null; } } $conn_w = $repository->establishConnection('w'); $changes_sql = array(); foreach ($changes as $change) { $values = array( (int)$change['repositoryID'], (int)$change['pathID'], (int)$change['commitID'], $change['targetPathID'] ? (int)$change['targetPathID'] : 'null', $change['targetCommitID'] ? (int)$change['targetCommitID'] : 'null', (int)$change['changeType'], (int)$change['fileType'], (int)$change['isDirect'], (int)$change['commitSequence'], ); $changes_sql[] = '('.implode(', ', $values).')'; } queryfx( $conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit->getID()); foreach (array_chunk($changes_sql, 256) as $sql_chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, implode(', ', $sql_chunk)); } } } diff --git a/src/applications/repository/worker/commitchangeparser/svn/PhabricatorRepositorySvnCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/svn/PhabricatorRepositorySvnCommitChangeParserWorker.php index 8e55474a1d..fcbbf5b67c 100644 --- a/src/applications/repository/worker/commitchangeparser/svn/PhabricatorRepositorySvnCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/svn/PhabricatorRepositorySvnCommitChangeParserWorker.php @@ -1,728 +1,734 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class PhabricatorRepositorySvnCommitChangeParserWorker extends PhabricatorRepositoryCommitChangeParserWorker { protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { // PREAMBLE: This class is absurdly complicated because it is very difficult // to get the information we need out of SVN. The actual data we need is: // // 1. Recursively, what were the affected paths? // 2. For each affected path, is it a file or a directory? // 3. How was each path affected (e.g. add, delete, move, copy)? // // We spend nearly all of our effort figuring out (1) and (2) because // "svn log" is not recursive and does not give us file/directory // information (that is, it will report a directory move as a single move, // even if many thousands of paths are affected). // // Instead, we have to "svn ls -R" the location of each path in its previous // life to figure out whether it is a file or a directory and exactly which // recursive paths were affected if it was moved or copied. This is very // complicated and has many special cases. $uri = $repository->getDetail('remote-uri'); $svn_commit = $commit->getCommitIdentifier(); $callsign = $repository->getCallsign(); - echo "Parsing r{$callsign}{$svn_commit}...\n"; + $full_name = 'r'.$callsign.$svn_commit; + echo "Parsing {$full_name}...\n"; + + if ($this->isBadCommit($full_name)) { + echo "This commit is marked bad!\n"; + return; + } // Pull the top-level path changes out of "svn log". This is pretty // straightforward; just parse the XML log. $log = $this->getSVNLogXMLObject($uri, $svn_commit); $entry = $log->logentry[0]; if (!$entry->paths) { // TODO: Explicitly mark this commit as broken elsewhere? This isn't // supposed to happen but we have some cases like rE27 and rG935 in the // Facebook repositories where things got all clowned up. return; } $raw_paths = array(); foreach ($entry->paths->path as $path) { $name = trim((string)$path); $raw_paths[$name] = array( 'rawPath' => $name, 'rawTargetPath' => (string)$path['copyfrom-path'], 'rawChangeType' => (string)$path['action'], 'rawTargetCommit' => (string)$path['copyfrom-rev'], ); } $copied_or_moved_map = array(); $deleted_paths = array(); $add_paths = array(); foreach ($raw_paths as $path => $raw_info) { if ($raw_info['rawTargetPath']) { $copied_or_moved_map[$raw_info['rawTargetPath']][] = $raw_info; } switch ($raw_info['rawChangeType']) { case 'D': $deleted_paths[$path] = $raw_info; break; case 'A': $add_paths[$path] = $raw_info; break; } } // If a path was deleted, we need to look in the repository history to // figure out where the former valid location for it is so we can figure out // if it was a directory or not, among other things. $lookup_here = array(); foreach ($raw_paths as $path => $raw_info) { if ($raw_info['rawChangeType'] != 'D') { continue; } // If a change copies a directory and then deletes something from it, // we need to look at the old location for information about the path, not // the new location. This workflow is pretty ridiculous -- so much so that // Trac gets it wrong. See Facebook rO6 for an example, if you happen to // work at Facebook. $parents = $this->expandAllParentPaths($path, $include_self = true); foreach ($parents as $parent) { if (isset($add_paths[$parent])) { $relative_path = substr($path, strlen($parent)); $lookup_here[$path] = array( 'rawPath' => $add_paths[$parent]['rawTargetPath'].$relative_path, 'rawCommit' => $add_paths[$parent]['rawTargetCommit'], ); continue 2; } } // Otherwise we can just look at the previous revision. $lookup_here[$path] = array( 'rawPath' => $path, 'rawCommit' => $svn_commit - 1, ); } $lookup = array(); foreach ($raw_paths as $path => $raw_info) { if ($raw_info['rawChangeType'] == 'D') { $lookup[$path] = $lookup_here[$path]; } else { // For everything that wasn't deleted, we can just look it up directly. $lookup[$path] = array( 'rawPath' => $path, 'rawCommit' => $svn_commit, ); } } $path_file_types = $this->lookupPathFileTypes($repository, $lookup); $effects = array(); $resolved_types = array(); $supplemental = array(); foreach ($raw_paths as $path => $raw_info) { if (isset($resolved_types[$path])) { $type = $resolved_types[$path]; } else { switch ($raw_info['rawChangeType']) { case 'D': if (isset($copied_or_moved_map[$path])) { if (count($copied_or_moved_map[$path]) > 1) { $type = DifferentialChangeType::TYPE_MULTICOPY; } else { $type = DifferentialChangeType::TYPE_MOVE_AWAY; } } else { $type = DifferentialChangeType::TYPE_DELETE; $file_type = $path_file_types[$path]; if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { // Bad. Child paths aren't enumerated in "svn log" so we need // to go fishing. $list = $this->lookupRecursiveFileList( $repository, $lookup[$path]); foreach ($list as $deleted_path => $path_file_type) { $deleted_path = rtrim($path.'/'.$deleted_path, '/'); if (!empty($raw_paths[$deleted_path])) { // We somehow learned about this deletion explicitly? // TODO: Unclear how this is possible. continue; } $effects[$deleted_path] = array( 'rawPath' => $deleted_path, 'rawTargetPath' => null, 'rawTargetCommit' => null, 'rawDirect' => true, 'changeType' => $type, 'fileType' => $path_file_type, ); } } } break; case 'A': $copy_from = $raw_info['rawTargetPath']; $copy_rev = $raw_info['rawTargetCommit']; if (!strlen($copy_from)) { $type = DifferentialChangeType::TYPE_ADD; } else { if (isset($deleted_paths[$copy_from])) { $type = DifferentialChangeType::TYPE_MOVE_HERE; $other_type = DifferentialChangeType::TYPE_MOVE_AWAY; } else { $type = DifferentialChangeType::TYPE_COPY_HERE; $other_type = DifferentialChangeType::TYPE_COPY_AWAY; } $source_file_type = $this->lookupPathFileType( $repository, $copy_from, array( 'rawPath' => $copy_from, 'rawCommit' => $copy_rev, )); if ($source_file_type == DifferentialChangeType::FILE_DELETED) { throw new Exception( "Something is wrong; source of a copy must exist."); } if ($source_file_type != DifferentialChangeType::FILE_DIRECTORY) { if (isset($raw_paths[$copy_from])) { break; } $effects[$copy_from] = array( 'rawPath' => $copy_from, 'rawTargetPath' => null, 'rawTargetCommit' => null, 'rawDirect' => false, 'changeType' => $other_type, 'fileType' => $source_file_type, ); } else { // ULTRADISASTER. We've added a directory which was copied // or moved from somewhere else. This is the most complex and // ridiculous case. $list = $this->lookupRecursiveFileList( $repository, array( 'rawPath' => $copy_from, 'rawCommit' => $copy_rev, )); foreach ($list as $from_path => $from_file_type) { $full_from = rtrim($copy_from.'/'.$from_path, '/'); $full_to = rtrim($path.'/'.$from_path, '/'); if (empty($raw_paths[$full_to])) { $effects[$full_to] = array( 'rawPath' => $full_to, 'rawTargetPath' => $full_from, 'rawTargetCommit' => $copy_rev, 'rawDirect' => false, 'changeType' => $type, 'fileType' => $from_file_type, ); } else { // This means we picked the file up explicitly elsewhere. // If the file as modified, SVN will drop the copy // information. We need to restore it. $supplemental[$full_to]['rawTargetPath'] = $full_from; $supplemental[$full_to]['rawTargetCommit'] = $copy_rev; if ($raw_paths[$full_to]['rawChangeType'] == 'M') { $resolved_types[$full_to] = $type; } } if (empty($raw_paths[$full_from])) { if ($other_type == DifferentialChangeType::TYPE_COPY_AWAY) { $effects[$full_from] = array( 'rawPath' => $full_from, 'rawTargetPath' => null, 'rawTargetCommit' => null, 'rawDirect' => false, 'changeType' => $other_type, 'fileType' => $from_file_type, ); } } } } } break; // This is "replaced", caused by "svn rm"-ing a file, putting another // in its place, and then "svn add"-ing it. We do not distinguish // between this and "M". case 'R': case 'M': if (isset($copied_or_moved_map[$path])) { $type = DifferentialChangeType::TYPE_COPY_AWAY; } else { $type = DifferentialChangeType::TYPE_CHANGE; } break; } } $resolved_types[$path] = $type; } foreach ($raw_paths as $path => $raw_info) { $raw_paths[$path]['changeType'] = $resolved_types[$path]; if (isset($supplemental[$path])) { foreach ($supplemental[$path] as $key => $value) { $raw_paths[$path][$key] = $value; } } } foreach ($raw_paths as $path => $raw_info) { $effects[$path] = array( 'rawPath' => $path, 'rawTargetPath' => $raw_info['rawTargetPath'], 'rawTargetCommit' => $raw_info['rawTargetCommit'], 'rawDirect' => true, 'changeType' => $raw_info['changeType'], 'fileType' => $path_file_types[$path], ); } $parents = array(); foreach ($effects as $path => $effect) { foreach ($this->expandAllParentPaths($path) as $parent_path) { $parents[$parent_path] = true; } } $parents = array_keys($parents); foreach ($parents as $parent) { if (isset($effects[$parent])) { continue; } $effects[$parent] = array( 'rawPath' => $parent, 'rawTargetPath' => null, 'rawTargetCommit' => null, 'rawDirect' => false, 'changeType' => DifferentialChangeType::TYPE_CHILD, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, ); } $lookup_paths = array(); foreach ($effects as $effect) { $lookup_paths[$effect['rawPath']] = true; if ($effect['rawTargetPath']) { $lookup_paths[$effect['rawTargetPath']] = true; } } $lookup_paths = array_keys($lookup_paths); $lookup_commits = array(); foreach ($effects as $effect) { if ($effect['rawTargetCommit']) { $lookup_commits[$effect['rawTargetCommit']] = true; } } $lookup_commits = array_keys($lookup_commits); $path_map = $this->lookupOrCreatePaths($lookup_paths); $commit_map = $this->lookupSvnCommits($repository, $lookup_commits); $this->writeChanges($repository, $commit, $effects, $path_map, $commit_map); $this->writeBrowse($repository, $commit, $effects, $path_map); } private function writeChanges( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $effects, array $path_map, array $commit_map) { $conn_w = $repository->establishConnection('w'); $sql = array(); foreach ($effects as $effect) { $sql[] = qsprintf( $conn_w, '(%d, %d, %d, %nd, %nd, %d, %d, %d, %d)', $repository->getID(), $path_map[$effect['rawPath']], $commit->getID(), $effect['rawTargetPath'] ? $path_map[$effect['rawTargetPath']] : null, $effect['rawTargetCommit'] ? $commit_map[$effect['rawTargetCommit']] : null, $effect['changeType'], $effect['fileType'], $effect['rawDirect'] ? 1 : 0, $commit->getCommitIdentifier()); } queryfx( $conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit->getID()); foreach (array_chunk($sql, 512) as $sql_chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, implode(', ', $sql_chunk)); } } private function writeBrowse( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $effects, array $path_map) { $conn_w = $repository->establishConnection('w'); $sql = array(); foreach ($effects as $effect) { $type = $effect['changeType']; if (!$effect['rawDirect']) { if ($type == DifferentialChangeType::TYPE_COPY_AWAY) { // Don't write COPY_AWAY to the filesystem table if it isn't a direct // event. continue; } if ($type == DifferentialChangeType::TYPE_CHILD) { // Don't write CHILD to the filesystem table. Although doing these // writes has the nice property of letting you see when a directory's // contents were last changed, it explodes the table tremendously // and makes Diffusion far slower. continue; } } if ($effect['rawPath'] == '/') { // Don't write any events on '/' to the filesystem table; in // particular, it doesn't have a meaningful parentID. continue; } $existed = !DifferentialChangeType::isDeleteChangeType($type); $sql[] = qsprintf( $conn_w, '(%d, %d, %d, %d, %d, %d)', $repository->getID(), $path_map[$this->getParentPath($effect['rawPath'])], $commit->getCommitIdentifier(), $path_map[$effect['rawPath']], $existed ? 1 : 0, $effect['fileType']); } queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryID = %d AND svnCommit = %d', PhabricatorRepository::TABLE_FILESYSTEM, $repository->getID(), $commit->getCommitIdentifier()); foreach (array_chunk($sql, 512) as $sql_chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, parentID, svnCommit, pathID, existed, fileType) VALUES %Q', PhabricatorRepository::TABLE_FILESYSTEM, implode(', ', $sql_chunk)); } } private function lookupSvnCommits( PhabricatorRepository $repository, array $commits) { if (!$commits) { return array(); } $commit_table = new PhabricatorRepositoryCommit(); $commit_data = queryfx_all( $commit_table->establishConnection('w'), 'SELECT id, commitIdentifier FROM %T WHERE commitIdentifier in (%Ld)', $commit_table->getTableName(), $commits); return ipull($commit_data, 'id', 'commitIdentifier'); } private function lookupPathFileType( PhabricatorRepository $repository, $path, array $path_info) { $result = $this->lookupPathFileTypes( $repository, array( $path => $path_info, )); return $result[$path]; } private function lookupPathFileTypes( PhabricatorRepository $repository, array $paths) { $repository_uri = $repository->getDetail('remote-uri'); $parents = array(); $path_mapping = array(); foreach ($paths as $path => $lookup) { $parent = dirname($lookup['rawPath']); $parent = ltrim($parent, '/'); $parent = $this->encodeSVNPath($parent); $parent = $repository_uri.$parent.'@'.$lookup['rawCommit']; $parent = escapeshellarg($parent); $parents[$parent] = true; $path_mapping[$parent][] = dirname($path); } $result_map = array(); // Reverse this list so we can pop $path_mapping, as that's more efficient // than shifting it. We need to associate these maps positionally because // a change can copy the same source path from multiple revisions via // "svn cp path@1 a; svn cp path@2 b;" and the XML output gives us no way // to distinguish which revision we're looking at except based on its // position in the document. $all_paths = array_reverse(array_keys($parents)); foreach (array_chunk($all_paths, 64) as $path_chunk) { list($raw_xml) = execx( 'svn --non-interactive --xml ls %C', implode(' ', $path_chunk)); $xml = new SimpleXMLElement($raw_xml); foreach ($xml->list as $list) { $list_path = (string)$list['path']; // SVN is a big mess. See Facebook rG8 (a revision which adds files // with spaces in their names) for an example. $list_path = rawurldecode($list_path); if ($list_path == $repository_uri) { $base = '/'; } else { $base = substr($list_path, strlen($repository_uri)); } $mapping = array_pop($path_mapping); foreach ($list->entry as $entry) { $val = $this->getFileTypeFromSVNKind($entry['kind']); foreach ($mapping as $base_path) { // rtrim() causes us to handle top-level directories correctly. $key = rtrim($base_path, '/').'/'.$entry->name; $result_map[$key] = $val; } } } } foreach ($paths as $path => $lookup) { if (empty($result_map[$path])) { $result_map[$path] = DifferentialChangeType::FILE_DELETED; } } return $result_map; } private function encodeSVNPath($path) { $path = rawurlencode($path); $path = str_replace('%2F', '/', $path); return $path; } private function getFileTypeFromSVNKind($kind) { $kind = (string)$kind; switch ($kind) { case 'dir': return DifferentialChangeType::FILE_DIRECTORY; case 'file': return DifferentialChangeType::FILE_NORMAL; default: throw new Exception("Unknown SVN file kind '{$kind}'."); } } private function lookupRecursiveFileList( PhabricatorRepository $repository, array $info) { $path = $info['rawPath']; $rev = $info['rawCommit']; $path = $this->encodeSVNPath($path); $hashkey = md5($repository->getDetail('remote-uri').$path.'@'.$rev); // This method is quite horrible. The underlying challenge is that some // commits in the Facebook repository are enormous, taking multiple hours // to 'ls -R' out of the repository and producing XML files >1GB in size. // If we try to SimpleXML them, the object exhausts available memory on a // 64G machine. Instead, cache the XML output and then parse it line by line // to limit space requirements. $cache_loc = sys_get_temp_dir().'/diffusion.'.$hashkey.'.svnls'; if (!Filesystem::pathExists($cache_loc)) { $tmp = new TempFile(); execx( 'svn --non-interactive --xml ls -R %s%s@%d > %s', $repository->getDetail('remote-uri'), $path, $rev, $tmp); execx( 'mv %s %s', $tmp, $cache_loc); } $map = $this->parseRecursiveListFileData($cache_loc); Filesystem::remove($cache_loc); return $map; } private function parseRecursiveListFileData($file_path) { $map = array(); $mode = 'xml'; $done = false; $entry = null; foreach (new LinesOfALargeFile($file_path) as $lno => $line) { switch ($mode) { case 'entry': if ($line == '</entry>') { $entry = implode('', $entry); $pattern = '@^\s+kind="(file|dir)">'. '<name>(.*?)</name>'. '(<size>(.*?)</size>)?@'; $matches = null; if (!preg_match($pattern, $entry, $matches)) { throw new Exception("Unable to parse entry!"); } $map[html_entity_decode($matches[2])] = $this->getFileTypeFromSVNKind($matches[1]); $mode = 'entry-or-end'; } else { $entry[] = $line; } break; case 'entry-or-end': if ($line == '</list>') { $done = true; break 2; } else if ($line == '<entry') { $mode = 'entry'; $entry = array(); } else { throw new Exception("Expected </list> or <entry, got {$line}."); } break; case 'xml': $expect = '<?xml version="1.0"?>'; if ($line !== $expect) { throw new Exception("Expected '{$expect}', got {$line}."); } $mode = 'list'; break; case 'list': $expect = '<lists>'; if ($line !== $expect) { throw new Exception("Expected '{$expect}', got {$line}."); } $mode = 'list1'; break; case 'list1': $expect = '<list'; if ($line !== $expect) { throw new Exception("Expected '{$expect}', got {$line}."); } $mode = 'list2'; break; case 'list2': if (!preg_match('/^\s+path="/', $line)) { throw new Exception("Expected ' path=...', got {$line}."); } $mode = 'entry-or-end'; break; } } if (!$done) { throw new Exception("Unexpected end of file."); } return $map; } private function getParentPath($path) { $path = rtrim($path, '/'); $path = dirname($path); if (!$path) { $path = '/'; } return $path; } private function expandAllParentPaths($path, $include_self = false) { $parents = array(); if ($include_self) { $parents[] = '/'.rtrim($path, '/'); } $parts = explode('/', trim($path, '/')); while (count($parts) >= 1) { array_pop($parts); $parents[] = '/'.implode('/', $parts); } return $parents; } }