[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/diffusion/conduit/ -> DiffusionBrowseQueryConduitAPIMethod.php (source)

   1  <?php
   2  
   3  final class DiffusionBrowseQueryConduitAPIMethod
   4    extends DiffusionQueryConduitAPIMethod {
   5  
   6    public function getAPIMethodName() {
   7      return 'diffusion.browsequery';
   8    }
   9  
  10    public function getMethodDescription() {
  11      return
  12        'File(s) information for a repository at an (optional) path and '.
  13        '(optional) commit.';
  14    }
  15  
  16    public function defineReturnType() {
  17      return 'array';
  18    }
  19  
  20    protected function defineCustomParamTypes() {
  21      return array(
  22        'path' => 'optional string',
  23        'commit' => 'optional string',
  24        'needValidityOnly' => 'optional bool',
  25      );
  26    }
  27  
  28    protected function getResult(ConduitAPIRequest $request) {
  29      $result = parent::getResult($request);
  30      return $result->toDictionary();
  31    }
  32  
  33    protected function getGitResult(ConduitAPIRequest $request) {
  34      $drequest = $this->getDiffusionRequest();
  35      $repository = $drequest->getRepository();
  36      $path = $request->getValue('path');
  37      $commit = $request->getValue('commit');
  38      $result = $this->getEmptyResultSet();
  39  
  40      if ($path == '') {
  41        // Fast path to improve the performance of the repository view; we know
  42        // the root is always a tree at any commit and always exists.
  43        $stdout = 'tree';
  44      } else {
  45        try {
  46          list($stdout) = $repository->execxLocalCommand(
  47            'cat-file -t %s:%s',
  48            $commit,
  49            $path);
  50        } catch (CommandException $e) {
  51          $stderr = $e->getStdErr();
  52          if (preg_match('/^fatal: Not a valid object name/', $stderr)) {
  53            // Grab two logs, since the first one is when the object was deleted.
  54            list($stdout) = $repository->execxLocalCommand(
  55              'log -n2 --format="%%H" %s -- %s',
  56              $commit,
  57              $path);
  58            $stdout = trim($stdout);
  59            if ($stdout) {
  60              $commits = explode("\n", $stdout);
  61              $result
  62                ->setReasonForEmptyResultSet(
  63                  DiffusionBrowseResultSet::REASON_IS_DELETED)
  64                ->setDeletedAtCommit(idx($commits, 0))
  65                ->setExistedAtCommit(idx($commits, 1));
  66              return $result;
  67            }
  68  
  69            $result->setReasonForEmptyResultSet(
  70              DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
  71            return $result;
  72          } else {
  73            throw $e;
  74          }
  75        }
  76      }
  77  
  78      if (trim($stdout) == 'blob') {
  79        $result->setReasonForEmptyResultSet(
  80          DiffusionBrowseResultSet::REASON_IS_FILE);
  81        return $result;
  82      }
  83  
  84      $result->setIsValidResults(true);
  85      if ($this->shouldOnlyTestValidity($request)) {
  86        return $result;
  87      }
  88  
  89      list($stdout) = $repository->execxLocalCommand(
  90        'ls-tree -z -l %s:%s',
  91        $commit,
  92        $path);
  93  
  94      $submodules = array();
  95  
  96      if (strlen($path)) {
  97        $prefix = rtrim($path, '/').'/';
  98      } else {
  99        $prefix = '';
 100      }
 101  
 102      $results = array();
 103      foreach (explode("\0", rtrim($stdout)) as $line) {
 104        // NOTE: Limit to 5 components so we parse filenames with spaces in them
 105        // correctly.
 106        // NOTE: The output uses a mixture of tabs and one-or-more spaces to
 107        // delimit fields.
 108        $parts = preg_split('/\s+/', $line, 5);
 109        if (count($parts) < 5) {
 110          throw new Exception(
 111            pht(
 112              'Expected "<mode> <type> <hash> <size>\t<name>", for ls-tree of '.
 113              '"%s:%s", got: %s',
 114              $commit,
 115              $path,
 116              $line));
 117        }
 118  
 119        list($mode, $type, $hash, $size, $name) = $parts;
 120  
 121        $path_result = new DiffusionRepositoryPath();
 122  
 123        if ($type == 'tree') {
 124          $file_type = DifferentialChangeType::FILE_DIRECTORY;
 125        } else if ($type == 'commit') {
 126          $file_type = DifferentialChangeType::FILE_SUBMODULE;
 127          $submodules[] = $path_result;
 128        } else {
 129          $mode = intval($mode, 8);
 130          if (($mode & 0120000) == 0120000) {
 131            $file_type = DifferentialChangeType::FILE_SYMLINK;
 132          } else {
 133            $file_type = DifferentialChangeType::FILE_NORMAL;
 134          }
 135        }
 136  
 137        $path_result->setFullPath($prefix.$name);
 138        $path_result->setPath($name);
 139        $path_result->setHash($hash);
 140        $path_result->setFileType($file_type);
 141        $path_result->setFileSize($size);
 142  
 143        $results[] = $path_result;
 144      }
 145  
 146      // If we identified submodules, lookup the module info at this commit to
 147      // find their source URIs.
 148  
 149      if ($submodules) {
 150  
 151        // NOTE: We need to read the file out of git and write it to a temporary
 152        // location because "git config -f" doesn't accept a "commit:path"-style
 153        // argument.
 154  
 155        // NOTE: This file may not exist, e.g. because the commit author removed
 156        // it when they added the submodule. See T1448. If it's not present, just
 157        // show the submodule without enriching it. If ".gitmodules" was removed
 158        // it seems to partially break submodules, but the repository as a whole
 159        // continues to work fine and we've seen at least two cases of this in
 160        // the wild.
 161  
 162        list($err, $contents) = $repository->execLocalCommand(
 163          'cat-file blob %s:.gitmodules',
 164          $commit);
 165  
 166        if (!$err) {
 167          $tmp = new TempFile();
 168          Filesystem::writeFile($tmp, $contents);
 169          list($module_info) = $repository->execxLocalCommand(
 170            'config -l -f %s',
 171            $tmp);
 172  
 173          $dict = array();
 174          $lines = explode("\n", trim($module_info));
 175          foreach ($lines as $line) {
 176            list($key, $value) = explode('=', $line, 2);
 177            $parts = explode('.', $key);
 178            $dict[$key] = $value;
 179          }
 180  
 181          foreach ($submodules as $path) {
 182            $full_path = $path->getFullPath();
 183            $key = 'submodule.'.$full_path.'.url';
 184            if (isset($dict[$key])) {
 185              $path->setExternalURI($dict[$key]);
 186            }
 187          }
 188        }
 189      }
 190  
 191      return $result->setPaths($results);
 192    }
 193  
 194    protected function getMercurialResult(ConduitAPIRequest $request) {
 195      $drequest = $this->getDiffusionRequest();
 196      $repository = $drequest->getRepository();
 197      $path = $request->getValue('path');
 198      $commit = $request->getValue('commit');
 199      $result = $this->getEmptyResultSet();
 200  
 201  
 202      $entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery())
 203        ->setRepository($repository)
 204        ->withCommit($commit)
 205        ->withPath($path)
 206        ->execute();
 207  
 208      $results = array();
 209  
 210      $match_against = trim($path, '/');
 211      $match_len = strlen($match_against);
 212  
 213      // For the root, don't trim. For other paths, trim the "/" after we match.
 214      // We need this because Mercurial's canonical paths have no leading "/",
 215      // but ours do.
 216      $trim_len = $match_len ? $match_len + 1 : 0;
 217  
 218      foreach ($entire_manifest as $path) {
 219        if (strncmp($path, $match_against, $match_len)) {
 220          continue;
 221        }
 222        if (!strlen($path)) {
 223          continue;
 224        }
 225        $remainder = substr($path, $trim_len);
 226        if (!strlen($remainder)) {
 227          // There is a file with this exact name in the manifest, so clearly
 228          // it's a file.
 229          $result->setReasonForEmptyResultSet(
 230            DiffusionBrowseResultSet::REASON_IS_FILE);
 231          return $result;
 232        }
 233        $parts = explode('/', $remainder);
 234        if (count($parts) == 1) {
 235          $type = DifferentialChangeType::FILE_NORMAL;
 236        } else {
 237          $type = DifferentialChangeType::FILE_DIRECTORY;
 238        }
 239        $results[reset($parts)] = $type;
 240      }
 241  
 242      foreach ($results as $key => $type) {
 243        $path_result = new DiffusionRepositoryPath();
 244        $path_result->setPath($key);
 245        $path_result->setFileType($type);
 246        $path_result->setFullPath(ltrim($match_against.'/', '/').$key);
 247  
 248        $results[$key] = $path_result;
 249      }
 250  
 251      $valid_results = true;
 252      if (empty($results)) {
 253        // TODO: Detect "deleted" by issuing "hg log"?
 254        $result->setReasonForEmptyResultSet(
 255          DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
 256        $valid_results = false;
 257      }
 258  
 259      return $result
 260        ->setPaths($results)
 261        ->setIsValidResults($valid_results);
 262    }
 263  
 264    protected function getSVNResult(ConduitAPIRequest $request) {
 265      $drequest = $this->getDiffusionRequest();
 266      $repository = $drequest->getRepository();
 267      $path = $request->getValue('path');
 268      $commit = $request->getValue('commit');
 269      $result = $this->getEmptyResultSet();
 270  
 271      $subpath = $repository->getDetail('svn-subpath');
 272      if ($subpath && strncmp($subpath, $path, strlen($subpath))) {
 273        // If we have a subpath and the path isn't a child of it, it (almost
 274        // certainly) won't exist since we don't track commits which affect
 275        // it. (Even if it exists, return a consistent result.)
 276        $result->setReasonForEmptyResultSet(
 277          DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT);
 278        return $result;
 279      }
 280  
 281      $conn_r = $repository->establishConnection('r');
 282  
 283      $parent_path = DiffusionPathIDQuery::getParentPath($path);
 284      $path_query = new DiffusionPathIDQuery(
 285        array(
 286          $path,
 287          $parent_path,
 288        ));
 289      $path_map = $path_query->loadPathIDs();
 290  
 291      $path_id = $path_map[$path];
 292      $parent_path_id = $path_map[$parent_path];
 293  
 294      if (empty($path_id)) {
 295        $result->setReasonForEmptyResultSet(
 296          DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
 297        return $result;
 298      }
 299  
 300      if ($commit) {
 301        $slice_clause = 'AND svnCommit <= '.(int)$commit;
 302      } else {
 303        $slice_clause = '';
 304      }
 305  
 306      $index = queryfx_all(
 307        $conn_r,
 308        'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE
 309          repositoryID = %d AND parentID = %d
 310          %Q GROUP BY pathID',
 311        PhabricatorRepository::TABLE_FILESYSTEM,
 312        $repository->getID(),
 313        $path_id,
 314        $slice_clause);
 315  
 316      if (!$index) {
 317        if ($path == '/') {
 318          $result->setReasonForEmptyResultSet(
 319            DiffusionBrowseResultSet::REASON_IS_EMPTY);
 320        } else {
 321  
 322          // NOTE: The parent path ID is included so this query can take
 323          // advantage of the table's primary key; it is uniquely determined by
 324          // the pathID but if we don't do the lookup ourselves MySQL doesn't have
 325          // the information it needs to avoid a table scan.
 326  
 327          $reasons = queryfx_all(
 328            $conn_r,
 329            'SELECT * FROM %T WHERE repositoryID = %d
 330                AND parentID = %d
 331                AND pathID = %d
 332              %Q ORDER BY svnCommit DESC LIMIT 2',
 333            PhabricatorRepository::TABLE_FILESYSTEM,
 334            $repository->getID(),
 335            $parent_path_id,
 336            $path_id,
 337            $slice_clause);
 338  
 339          $reason = reset($reasons);
 340  
 341          if (!$reason) {
 342            $result->setReasonForEmptyResultSet(
 343              DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
 344          } else {
 345            $file_type = $reason['fileType'];
 346            if (empty($reason['existed'])) {
 347              $result->setReasonForEmptyResultSet(
 348                DiffusionBrowseResultSet::REASON_IS_DELETED);
 349              $result->setDeletedAtCommit($reason['svnCommit']);
 350              if (!empty($reasons[1])) {
 351                $result->setExistedAtCommit($reasons[1]['svnCommit']);
 352              }
 353            } else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
 354              $result->setReasonForEmptyResultSet(
 355                DiffusionBrowseResultSet::REASON_IS_EMPTY);
 356            } else {
 357              $result->setReasonForEmptyResultSet(
 358                DiffusionBrowseResultSet::REASON_IS_FILE);
 359            }
 360          }
 361        }
 362        return $result;
 363      }
 364  
 365      $result->setIsValidResults(true);
 366      if ($this->shouldOnlyTestValidity($request)) {
 367        return $result;
 368      }
 369  
 370      $sql = array();
 371      foreach ($index as $row) {
 372        $sql[] =
 373          '(pathID = '.(int)$row['pathID'].' AND '.
 374          'svnCommit = '.(int)$row['maxCommit'].')';
 375      }
 376  
 377      $browse = queryfx_all(
 378        $conn_r,
 379        'SELECT *, p.path pathName
 380          FROM %T f JOIN %T p ON f.pathID = p.id
 381          WHERE repositoryID = %d
 382            AND parentID = %d
 383            AND existed = 1
 384          AND (%Q)
 385          ORDER BY pathName',
 386        PhabricatorRepository::TABLE_FILESYSTEM,
 387        PhabricatorRepository::TABLE_PATH,
 388        $repository->getID(),
 389        $path_id,
 390        implode(' OR ', $sql));
 391  
 392      $loadable_commits = array();
 393      foreach ($browse as $key => $file) {
 394        // We need to strip out directories because we don't store last-modified
 395        // in the filesystem table.
 396        if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) {
 397          $loadable_commits[] = $file['svnCommit'];
 398          $browse[$key]['hasCommit'] = true;
 399        }
 400      }
 401  
 402      $commits = array();
 403      $commit_data = array();
 404      if ($loadable_commits) {
 405        // NOTE: Even though these are integers, use '%Ls' because MySQL doesn't
 406        // use the second part of the key otherwise!
 407        $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
 408          'repositoryID = %d AND commitIdentifier IN (%Ls)',
 409          $repository->getID(),
 410          $loadable_commits);
 411        $commits = mpull($commits, null, 'getCommitIdentifier');
 412        if ($commits) {
 413          $commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
 414            'commitID in (%Ld)',
 415            mpull($commits, 'getID'));
 416          $commit_data = mpull($commit_data, null, 'getCommitID');
 417        } else {
 418          $commit_data = array();
 419        }
 420      }
 421  
 422      $path_normal = DiffusionPathIDQuery::normalizePath($path);
 423  
 424      $results = array();
 425      foreach ($browse as $file) {
 426  
 427        $full_path = $file['pathName'];
 428        $file_path = ltrim(substr($full_path, strlen($path_normal)), '/');
 429        $full_path = ltrim($full_path, '/');
 430  
 431        $result_path = new DiffusionRepositoryPath();
 432        $result_path->setPath($file_path);
 433        $result_path->setFullPath($full_path);
 434  //      $result_path->setHash($hash);
 435        $result_path->setFileType($file['fileType']);
 436  //      $result_path->setFileSize($size);
 437  
 438        if (!empty($file['hasCommit'])) {
 439          $commit = idx($commits, $file['svnCommit']);
 440          if ($commit) {
 441            $data = idx($commit_data, $commit->getID());
 442            $result_path->setLastModifiedCommit($commit);
 443            $result_path->setLastCommitData($data);
 444          }
 445        }
 446  
 447        $results[] = $result_path;
 448      }
 449  
 450      if (empty($results)) {
 451        $result->setReasonForEmptyResultSet(
 452          DiffusionBrowseResultSet::REASON_IS_EMPTY);
 453      }
 454  
 455      return $result->setPaths($results);
 456    }
 457  
 458    private function getEmptyResultSet() {
 459      return id(new DiffusionBrowseResultSet())
 460        ->setPaths(array())
 461        ->setReasonForEmptyResultSet(null)
 462        ->setIsValidResults(false);
 463    }
 464  
 465    private function shouldOnlyTestValidity(ConduitAPIRequest $request) {
 466      return $request->getValue('needValidityOnly', false);
 467    }
 468  
 469  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1