Page MenuHomePhorge

No OneTemporary

diff --git a/resources/sql/patches/008.repoopt.sql b/resources/sql/patches/008.repoopt.sql
new file mode 100644
index 0000000000..d20b6084b5
--- /dev/null
+++ b/resources/sql/patches/008.repoopt.sql
@@ -0,0 +1,5 @@
+ALTER TABLE phabricator_repository.repository_filesystem DROP PRIMARY KEY;
+ALTER TABLE phabricator_repository.repository_filesystem
+ DROP KEY repositoryID_2;
+ALTER TABLE phabricator_repository.repository_filesystem
+ ADD PRIMARY KEY (repositoryID, parentID, pathID, svnCommit);
diff --git a/src/applications/diffusion/controller/browse/DiffusionBrowseController.php b/src/applications/diffusion/controller/browse/DiffusionBrowseController.php
index 44074b0561..e1a1f4bfc8 100644
--- a/src/applications/diffusion/controller/browse/DiffusionBrowseController.php
+++ b/src/applications/diffusion/controller/browse/DiffusionBrowseController.php
@@ -1,104 +1,110 @@
<?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 DiffusionBrowseController extends DiffusionController {
public function processRequest() {
$drequest = $this->diffusionRequest;
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
$results = $browse_query->loadPaths();
$content = array();
$content[] = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
if (!$results) {
+ // TODO: Format all these commits into nice VCS-agnostic links, and
+ // below.
+ $commit = $drequest->getCommit();
+ $callsign = $drequest->getRepository()->getCallsign();
+ if ($commit) {
+ $commit = "r{$callsign}{$commit}";
+ } else {
+ $commit = 'HEAD';
+ }
+
switch ($browse_query->getReasonForEmptyResultSet()) {
case DiffusionBrowseQuery::REASON_IS_NONEXISTENT:
$title = 'Path Does Not Exist';
// TODO: Under git, this error message should be more specific. It
// may exist on some other branch.
$body = "This path does not exist anywhere.";
$severity = AphrontErrorView::SEVERITY_ERROR;
break;
+ case DiffusionBrowseQuery::REASON_IS_EMPTY:
+ $title = 'Empty Directory';
+ $body = "This path was an empty directory at {$commit}.\n";
+ $severity = AphrontErrorView::SEVERITY_NOTICE;
+ break;
case DiffusionBrowseQuery::REASON_IS_DELETED:
- // TODO: Format all these commits into nice VCS-agnostic links.
- $commit = $drequest->getCommit();
$deleted = $browse_query->getDeletedAtCommit();
$existed = $browse_query->getExistedAtCommit();
- $callsign = $drequest->getRepository()->getCallsign();
-
- if ($commit) {
- $commit = "r{$callsign}{$commit}";
- } else {
- $commit = 'HEAD';
- }
$deleted = "r{$callsign}{$deleted}";
$existed = "r{$callsign}{$existed}";
$title = 'Path Was Deleted';
$body = "This path does not exist at {$commit}. It was deleted in ".
"{$deleted} and last existed at {$existed}.";
$severity = AphrontErrorView::SEVERITY_WARNING;
break;
case DiffusionBrowseQuery::REASON_IS_FILE:
$controller = new DiffusionBrowseFileController($this->getRequest());
$controller->setDiffusionRequest($drequest);
return $this->delegateToController($controller);
break;
default:
throw new Exception("Unknown failure reason!");
}
$error_view = new AphrontErrorView();
$error_view->setSeverity($severity);
$error_view->setTitle($title);
$error_view->appendChild('<p>'.$body.'</p>');
$content[] = $error_view;
} else {
$browse_table = new DiffusionBrowseTableView();
$browse_table->setDiffusionRequest($drequest);
$browse_table->setPaths($results);
$browse_panel = new AphrontPanelView();
$browse_panel->appendChild($browse_table);
$content[] = $browse_panel;
}
$nav = $this->buildSideNav('browse', false);
$nav->appendChild($content);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => basename($drequest->getPath()),
));
}
}
diff --git a/src/applications/diffusion/controller/commit/DiffusionCommitController.php b/src/applications/diffusion/controller/commit/DiffusionCommitController.php
index 32bb7a6d2f..11a754c9ec 100644
--- a/src/applications/diffusion/controller/commit/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/commit/DiffusionCommitController.php
@@ -1,90 +1,94 @@
<?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();
$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().
' &middot; '.
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');
+ $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/query/browse/base/DiffusionBrowseQuery.php b/src/applications/diffusion/query/browse/base/DiffusionBrowseQuery.php
index b0b9cfd29b..c570585357 100644
--- a/src/applications/diffusion/query/browse/base/DiffusionBrowseQuery.php
+++ b/src/applications/diffusion/query/browse/base/DiffusionBrowseQuery.php
@@ -1,82 +1,83 @@
<?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 DiffusionBrowseQuery {
private $request;
protected $reason;
protected $existedAtCommit;
protected $deletedAtCommit;
const REASON_IS_FILE = 'is-file';
const REASON_IS_DELETED = 'is-deleted';
const REASON_IS_NONEXISTENT = 'nonexistent';
const REASON_BAD_COMMIT = 'bad-commit';
+ const REASON_IS_EMPTY = 'empty';
final private function __construct() {
// <private>
}
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
$repository = $request->getRepository();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// TODO: Verify local-path?
$class = 'DiffusionGitBrowseQuery';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$class = 'DiffusionSvnBrowseQuery';
break;
default:
throw new Exception("Unsupported VCS!");
}
PhutilSymbolLoader::loadClass($class);
$query = new $class();
$query->request = $request;
return $query;
}
final protected function getRequest() {
return $this->request;
}
final public function getReasonForEmptyResultSet() {
return $this->reason;
}
final public function getExistedAtCommit() {
return $this->existedAtCommit;
}
final public function getDeletedAtCommit() {
return $this->deletedAtCommit;
}
final public function loadPaths() {
return $this->executeQuery();
}
abstract protected function executeQuery();
}
diff --git a/src/applications/diffusion/query/pathchange/base/DiffusionPathChangeQuery.php b/src/applications/diffusion/query/pathchange/base/DiffusionPathChangeQuery.php
index 5cd79d2b27..ca7789b538 100644
--- a/src/applications/diffusion/query/pathchange/base/DiffusionPathChangeQuery.php
+++ b/src/applications/diffusion/query/pathchange/base/DiffusionPathChangeQuery.php
@@ -1,80 +1,81 @@
<?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 DiffusionPathChangeQuery {
private $request;
final private function __construct() {
// <private>
}
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
$query = new DiffusionPathChangeQuery();
$query->request = $request;
return $query;
}
final protected function getRequest() {
return $this->request;
}
final public function loadChanges() {
return $this->executeQuery();
}
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$commit = $drequest->loadCommit();
$raw_changes = queryfx_all(
$repository->establishConnection('r'),
'SELECT c.*, p.path pathName, t.path targetPathName
FROM %T c
LEFT JOIN %T p ON c.pathID = p.id
LEFT JOIN %T t on c.targetPathID = t.id
- WHERE c.commitID = %d',
+ WHERE c.commitID = %d AND isDirect = 1',
PhabricatorRepository::TABLE_PATHCHANGE,
PhabricatorRepository::TABLE_PATH,
PhabricatorRepository::TABLE_PATH,
$commit->getID());
$changes = array();
+
$raw_changes = isort($raw_changes, 'pathName');
foreach ($raw_changes as $raw_change) {
$type = $raw_change['changeType'];
if ($type == DifferentialChangeType::TYPE_CHILD) {
continue;
}
$change = new DiffusionPathChange();
$change->setPath(ltrim($raw_change['pathName'], '/'));
$change->setChangeType($raw_change['changeType']);
$change->setFileType($raw_change['fileType']);
$changes[] = $change;
}
return $changes;
}
}
diff --git a/src/applications/diffusion/request/base/DiffusionRequest.php b/src/applications/diffusion/request/base/DiffusionRequest.php
index d3758d36fe..064996de1c 100644
--- a/src/applications/diffusion/request/base/DiffusionRequest.php
+++ b/src/applications/diffusion/request/base/DiffusionRequest.php
@@ -1,140 +1,144 @@
<?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 DiffusionRequest {
protected $callsign;
protected $path;
protected $line;
protected $commit;
protected $branch;
protected $repository;
protected $repositoryCommit;
protected $repositoryCommitData;
final private function __construct() {
// <private>
}
final public static function newFromAphrontRequestDictionary(array $data) {
$vcs = null;
$repository = null;
$callsign = idx($data, 'callsign');
if ($callsign) {
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repository) {
throw new Exception("No such repository '{$callsign}'.");
}
$vcs = $repository->getVersionControlSystem();
}
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$class = 'DiffusionGitRequest';
break;
default:
$class = 'DiffusionRequest';
break;
}
$object = new $class();
$object->callsign = $callsign;
$object->repository = $repository;
$object->line = idx($data, 'line');
$object->commit = idx($data, 'commit');
$object->path = idx($data, 'path');
$object->initializeFromAphrontRequestDictionary();
return $object;
}
protected function initializeFromAphrontRequestDictionary() {
}
protected function parsePath($path) {
$this->path = $path;
}
public function getRepository() {
return $this->repository;
}
public function getCallsign() {
return $this->callsign;
}
public function getPath() {
return $this->path;
}
public function getLine() {
return $this->line;
}
public function getCommit() {
return $this->commit;
}
public function getBranch() {
return $this->branch;
}
public function loadCommit() {
if (empty($this->repositoryCommit)) {
$repository = $this->getRepository();
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$repository->getID(),
$this->getCommit());
$this->repositoryCommit = $commit;
}
return $this->repositoryCommit;
}
public function loadCommitData() {
if (empty($this->repositoryCommitData)) {
$commit = $this->loadCommit();
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
+ if (!$data) {
+ $data = new PhabricatorRepositoryCommitData();
+ $data->setCommitMessage('(This commit has not fully parsed yet.)');
+ }
$this->repositoryCommitData = $data;
}
return $this->repositoryCommitData;
}
final public function getRawCommit() {
return $this->commit;
}
public function getCommitURIComponent($commit) {
return $commit;
}
public function getBranchURIComponent($branch) {
return $branch;
}
}
diff --git a/src/applications/diffusion/view/commitchangetable/DiffusionCommitChangeTableView.php b/src/applications/diffusion/view/commitchangetable/DiffusionCommitChangeTableView.php
index 6b8d90c8e8..bfbf911e3a 100644
--- a/src/applications/diffusion/view/commitchangetable/DiffusionCommitChangeTableView.php
+++ b/src/applications/diffusion/view/commitchangetable/DiffusionCommitChangeTableView.php
@@ -1,66 +1,82 @@
<?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.
*/
final class DiffusionCommitChangeTableView extends DiffusionView {
private $pathChanges;
public function setPathChanges(array $path_changes) {
$this->pathChanges = $path_changes;
return $this;
}
public function render() {
$rows = array();
+
+ // TODO: Experiment with path stack rendering.
+
+ // TODO: Copy Away and Move Away are rendered junkily still.
+
foreach ($this->pathChanges as $change) {
$change_verb = DifferentialChangeType::getFullNameForChangeType(
$change->getChangeType());
+ $suffix = null;
+ if ($change->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
+ $suffix = '/';
+ }
+
+ $path = $change->getPath();
+ $hash = substr(sha1($path), 0, 7);
+
$rows[] = array(
$this->linkHistory($change->getPath()),
$this->linkBrowse($change->getPath()),
$this->linkChange(
- $change->getPath(),
+ $change->getChangeType(),
+ $change->getFileType()),
+ phutil_render_tag(
+ 'a',
array(
- 'text' => $change_verb,
- )),
- phutil_escape_html($change->getPath()),
+ 'href' => '#'.$hash,
+ ),
+ phutil_escape_html($path).$suffix),
);
}
$view = new AphrontTableView($rows);
$view->setHeaders(
array(
'History',
'Browse',
'Change',
'Path',
));
$view->setColumnClasses(
array(
'',
'',
'',
'wide',
));
$view->setNoDataString('This change has not been fully parsed yet.');
return $view->render();
}
}
diff --git a/src/applications/repository/worker/commitchangeparser/svn/PhabricatorRepositorySvnCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/svn/PhabricatorRepositorySvnCommitChangeParserWorker.php
index 5ce4d1ce94..c35f5a4121 100644
--- a/src/applications/repository/worker/commitchangeparser/svn/PhabricatorRepositorySvnCommitChangeParserWorker.php
+++ b/src/applications/repository/worker/commitchangeparser/svn/PhabricatorRepositorySvnCommitChangeParserWorker.php
@@ -1,720 +1,732 @@
<?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";
// Pull the top-level path changes out of "svn log". This is pretty
// straightforward; just parse the XML log.
list($xml) = execx(
'svn log --verbose --xml --limit 1 --non-interactive %s@%d',
$uri,
$svn_commit);
$log = new SimpleXMLElement($xml);
$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,
- $path,
+ $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' => true,
+ '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'];
- // Don't write COPY_AWAY to the filesystem table if it isn't a direct
- // event. We do write CHILD.
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 bother writing the CHILD events on '/' to the filesystem
- // table; in particular, it doesn't have a meaningful parentID.
+ // 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;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Mar 24, 00:25 (1 d, 4 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1259409
Default Alt Text
(45 KB)

Event Timeline