private function parseData($name, $data)
 {
     switch ($name) {
         case 'branches-basic.txt':
             $output = ArcanistMercurialParser::parseMercurialBranches($data);
             $this->assertEqual(array('default', 'stable'), array_keys($output));
             $this->assertEqual(array('a21ccf4412d5', 'ec222a29bdf0'), array_values(ipull($output, 'rev')));
             break;
         case 'branches-with-spaces.txt':
             $output = ArcanistMercurialParser::parseMercurialBranches($data);
             $this->assertEqual(array('m m m m m 2:ffffffffffff (inactive)', 'xxx yyy zzz', 'default', "'"), array_keys($output));
             $this->assertEqual(array('0b9d8290c4e0', '78963faacfc7', '5db03c5500c6', 'ffffffffffff'), array_values(ipull($output, 'rev')));
             break;
         case 'branches-empty.txt':
             $output = ArcanistMercurialParser::parseMercurialBranches($data);
             $this->assertEqual(array(), $output);
             break;
         case 'log-basic.txt':
             $output = ArcanistMercurialParser::parseMercurialLog($data);
             $this->assertEqual(3, count($output));
             $this->assertEqual(array('a21ccf4412d5', 'a051f8a6a7cc', 'b1f49efeab65'), array_values(ipull($output, 'rev')));
             break;
         case 'log-empty.txt':
             // Empty logs (e.g., "hg parents" for a root revision) should parse
             // correctly.
             $output = ArcanistMercurialParser::parseMercurialLog($data);
             $this->assertEqual(array(), $output);
             break;
         case 'status-basic.txt':
             $output = ArcanistMercurialParser::parseMercurialStatus($data);
             $this->assertEqual(4, count($output));
             $this->assertEqual(array('changed', 'added', 'removed', 'untracked'), array_keys($output));
             break;
         case 'status-moves.txt':
             $output = ArcanistMercurialParser::parseMercurialStatusDetails($data);
             $this->assertEqual('move_source', $output['moved_file']['from']);
             $this->assertEqual(null, $output['changed_file']['from']);
             $this->assertEqual('copy_source', $output['copied_file']['from']);
             $this->assertEqual(null, idx($output, 'copy_source'));
             break;
         default:
             throw new Exception(pht("No test information for test data '%s'!", $name));
     }
 }
 private function discoverCommit($commit)
 {
     $discover = array();
     $insert = array();
     $repository = $this->getRepository();
     $discover[] = $commit;
     $insert[] = $commit;
     $seen_parent = array();
     // For all the new commits at the branch heads, walk backward until we find
     // only commits we've aleady seen.
     while (true) {
         $target = array_pop($discover);
         list($stdout) = $repository->execxLocalCommand('parents --style default --rev %s', $target);
         $parents = ArcanistMercurialParser::parseMercurialLog($stdout);
         if ($parents) {
             foreach ($parents as $parent) {
                 $parent_commit = $parent['rev'];
                 $parent_commit = $this->getFullHash($parent_commit);
                 if (isset($seen_parent[$parent_commit])) {
                     continue;
                 }
                 $seen_parent[$parent_commit] = true;
                 if (!$this->isKnownCommit($parent_commit)) {
                     $discover[] = $parent_commit;
                     $insert[] = $parent_commit;
                 }
             }
         }
         if (empty($discover)) {
             break;
         }
         $this->stillWorking();
     }
     while (true) {
         $target = array_pop($insert);
         list($stdout) = $repository->execxLocalCommand('log --rev %s --template %s', $target, '{date|rfc822date}');
         $epoch = strtotime($stdout);
         $this->recordCommit($target, $epoch);
         if (empty($insert)) {
             break;
         }
     }
 }
Пример #3
0
 public function getLocalCommitInformation()
 {
     if ($this->localCommitInfo === null) {
         list($info) = $this->execxLocal('log --style default --rev %s..%s --', $this->getRelativeCommit(), $this->getWorkingCopyRevision());
         $logs = ArcanistMercurialParser::parseMercurialLog($info);
         // Get rid of the first log, it's not actually part of the diff. "hg log"
         // is inclusive, while "hg diff" is exclusive.
         array_shift($logs);
         // Expand short hashes (12 characters) to full hashes (40 characters) by
         // issuing a big "hg log" command. Possibly we should do this with parents
         // too, but nothing uses them directly at the moment.
         if ($logs) {
             $cmd = array();
             foreach (ipull($logs, 'rev') as $rev) {
                 $cmd[] = csprintf('--rev %s', $rev);
             }
             list($full) = $this->execxLocal('log --template %s %C --', '{node}\\n', implode(' ', $cmd));
             $full = explode("\n", trim($full));
             foreach ($logs as $key => $dict) {
                 $logs[$key]['rev'] = array_pop($full);
             }
         }
         $this->localCommitInfo = $logs;
     }
     return $this->localCommitInfo;
 }
Пример #4
0
 protected function buildBaseCommit($symbolic_commit)
 {
     if ($symbolic_commit !== null) {
         try {
             $commit = $this->getCanonicalRevisionName(hgsprintf('ancestor(%s,.)', $symbolic_commit));
         } catch (Exception $ex) {
             // Try it as a revset instead of a commit id
             try {
                 $commit = $this->getCanonicalRevisionName(hgsprintf('ancestor(%R,.)', $symbolic_commit));
             } catch (Exception $ex) {
                 throw new ArcanistUsageException(pht("Commit '%s' is not a valid Mercurial commit identifier.", $symbolic_commit));
             }
         }
         $this->setBaseCommitExplanation(pht('it is the greatest common ancestor of the working directory ' . 'and the commit you specified explicitly.'));
         return $commit;
     }
     if ($this->getBaseCommitArgumentRules() || $this->getConfigurationManager()->getConfigFromAnySource('base')) {
         $base = $this->resolveBaseCommit();
         if (!$base) {
             throw new ArcanistUsageException(pht("None of the rules in your 'base' configuration matched a valid " . "commit. Adjust rules or specify which commit you want to use " . "explicitly."));
         }
         return $base;
     }
     // Mercurial 2.1 and up have phases which indicate if something is
     // published or not. To find which revs are outgoing, it's much
     // faster to check the phase instead of actually checking the server.
     if ($this->supportsPhases()) {
         list($err, $stdout) = $this->execManualLocal('log --branch %s -r %s --style default', $this->getBranchName(), 'draft()');
     } else {
         list($err, $stdout) = $this->execManualLocal('outgoing --branch %s --style default', $this->getBranchName());
     }
     if (!$err) {
         $logs = ArcanistMercurialParser::parseMercurialLog($stdout);
     } else {
         // Mercurial (in some versions?) raises an error when there's nothing
         // outgoing.
         $logs = array();
     }
     if (!$logs) {
         $this->setBaseCommitExplanation(pht('you have no outgoing commits, so arc assumes you intend to submit ' . 'uncommitted changes in the working copy.'));
         return $this->getWorkingCopyRevision();
     }
     $outgoing_revs = ipull($logs, 'rev');
     // This is essentially an implementation of a theoretical `hg merge-base`
     // command.
     $against = $this->getWorkingCopyRevision();
     while (true) {
         // NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is
         // new as of July 2011, so do this in a compatible way. Also, "hg log"
         // and "hg outgoing" don't necessarily show parents (even if given an
         // explicit template consisting of just the parents token) so we need
         // to separately execute "hg parents".
         list($stdout) = $this->execxLocal('parents --style default --rev %s', $against);
         $parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
         list($p1, $p2) = array_merge($parents_logs, array(null, null));
         if ($p1 && !in_array($p1['rev'], $outgoing_revs)) {
             $against = $p1['rev'];
             break;
         } else {
             if ($p2 && !in_array($p2['rev'], $outgoing_revs)) {
                 $against = $p2['rev'];
                 break;
             } else {
                 if ($p1) {
                     $against = $p1['rev'];
                 } else {
                     // This is the case where you have a new repository and the entire
                     // thing is outgoing; Mercurial literally accepts "--rev null" as
                     // meaning "diff against the empty state".
                     $against = 'null';
                     break;
                 }
             }
         }
     }
     if ($against == 'null') {
         $this->setBaseCommitExplanation(pht('this is a new repository (all changes are outgoing).'));
     } else {
         $this->setBaseCommitExplanation(pht('it is the first commit reachable from the working copy state ' . 'which is not outgoing.'));
     }
     return $against;
 }
Пример #5
0
 public function getRelativeCommit()
 {
     if (empty($this->relativeCommit)) {
         if ($this->getBaseCommitArgumentRules() || $this->getWorkingCopyIdentity()->getConfigFromAnySource('base')) {
             $base = $this->resolveBaseCommit();
             if (!$base) {
                 throw new ArcanistUsageException("None of the rules in your 'base' configuration matched a valid " . "commit. Adjust rules or specify which commit you want to use " . "explicitly.");
             }
             $this->relativeCommit = $base;
             return $this->relativeCommit;
         }
         list($err, $stdout) = $this->execManualLocal('outgoing --branch %s --style default', $this->getBranchName());
         if (!$err) {
             $logs = ArcanistMercurialParser::parseMercurialLog($stdout);
         } else {
             // Mercurial (in some versions?) raises an error when there's nothing
             // outgoing.
             $logs = array();
         }
         if (!$logs) {
             $this->setBaseCommitExplanation("you have no outgoing commits, so arc assumes you intend to submit " . "uncommitted changes in the working copy.");
             // In Mercurial, we support operations against uncommitted changes.
             $this->setRelativeCommit($this->getWorkingCopyRevision());
             return $this->relativeCommit;
         }
         $outgoing_revs = ipull($logs, 'rev');
         // This is essentially an implementation of a theoretical `hg merge-base`
         // command.
         $against = $this->getWorkingCopyRevision();
         while (true) {
             // NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is
             // new as of July 2011, so do this in a compatible way. Also, "hg log"
             // and "hg outgoing" don't necessarily show parents (even if given an
             // explicit template consisting of just the parents token) so we need
             // to separately execute "hg parents".
             list($stdout) = $this->execxLocal('parents --style default --rev %s', $against);
             $parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
             list($p1, $p2) = array_merge($parents_logs, array(null, null));
             if ($p1 && !in_array($p1['rev'], $outgoing_revs)) {
                 $against = $p1['rev'];
                 break;
             } else {
                 if ($p2 && !in_array($p2['rev'], $outgoing_revs)) {
                     $against = $p2['rev'];
                     break;
                 } else {
                     if ($p1) {
                         $against = $p1['rev'];
                     } else {
                         // This is the case where you have a new repository and the entire
                         // thing is outgoing; Mercurial literally accepts "--rev null" as
                         // meaning "diff against the empty state".
                         $against = 'null';
                         break;
                     }
                 }
             }
         }
         if ($against == 'null') {
             $this->setBaseCommitExplanation("this is a new repository (all changes are outgoing).");
         } else {
             $this->setBaseCommitExplanation("it is the first commit reachable from the working copy state " . "which is not outgoing.");
         }
         $this->setRelativeCommit($against);
     }
     return $this->relativeCommit;
 }
 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;
     }
     list($stdout) = $repository->execxLocalCommand('status -C --change %s', $commit->getCommitIdentifier());
     $status = ArcanistMercurialParser::parseMercurialStatusDetails($stdout);
     $common_attributes = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'commitSequence' => $commit->getEpoch());
     $changes = array();
     // Like Git, Mercurial doesn't track directories directly. We need to infer
     // directory creation and removal by observing file creation and removal
     // and testing if the directories in question are previously empty (thus,
     // created) or subsequently empty (thus, removed).
     $maybe_new_directories = array();
     $maybe_del_directories = array();
     $all_directories = array();
     // Parse the basic information from "hg status", which shows files that
     // were directly affected by the change.
     foreach ($status as $path => $path_info) {
         $path = '/' . $path;
         $flags = $path_info['flags'];
         $change_target = $path_info['from'] ? '/' . $path_info['from'] : null;
         $changes[$path] = array('path' => $path, 'isDirect' => true, 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null, 'changeType' => null, 'fileType' => null, 'flags' => $flags) + $common_attributes;
         if ($flags & ArcanistRepositoryAPI::FLAG_ADDED) {
             $maybe_new_directories[] = dirname($path);
         } else {
             if ($flags & ArcanistRepositoryAPI::FLAG_DELETED) {
                 $maybe_del_directories[] = dirname($path);
             }
         }
         $all_directories[] = dirname($path);
     }
     // Add change information for each source path which doesn't appear in the
     // status. These files were copied, but were not modified. We also know they
     // must exist.
     foreach ($changes as $path => $change) {
         $from = $change['targetPath'];
         if ($from && empty($changes[$from])) {
             $changes[$from] = array('path' => $from, 'isDirect' => false, 'targetPath' => null, 'targetCommitID' => null, 'changeType' => DifferentialChangeType::TYPE_COPY_AWAY, 'fileType' => null, 'flags' => 0) + $common_attributes;
         }
     }
     $away = array();
     foreach ($changes as $path => $change) {
         $target_path = $change['targetPath'];
         if ($target_path) {
             $away[$target_path][] = $path;
         }
     }
     // Now that we have all the direct changes, figure out change types.
     foreach ($changes as $path => $change) {
         $flags = $change['flags'];
         $from = $change['targetPath'];
         if ($from) {
             $target = $changes[$from];
         } else {
             $target = null;
         }
         if ($flags & ArcanistRepositoryAPI::FLAG_ADDED) {
             if ($target) {
                 if ($target['flags'] & ArcanistRepositoryAPI::FLAG_DELETED) {
                     $change_type = DifferentialChangeType::TYPE_MOVE_HERE;
                 } else {
                     $change_type = DifferentialChangeType::TYPE_COPY_HERE;
                 }
             } else {
                 $change_type = DifferentialChangeType::TYPE_ADD;
             }
         } else {
             if ($flags & ArcanistRepositoryAPI::FLAG_DELETED) {
                 if (isset($away[$path])) {
                     if (count($away[$path]) > 1) {
                         $change_type = DifferentialChangeType::TYPE_MULTICOPY;
                     } else {
                         $change_type = DifferentialChangeType::TYPE_MOVE_AWAY;
                     }
                 } else {
                     $change_type = DifferentialChangeType::TYPE_DELETE;
                 }
             } else {
                 if (isset($away[$path])) {
                     $change_type = DifferentialChangeType::TYPE_COPY_AWAY;
                 } else {
                     $change_type = DifferentialChangeType::TYPE_CHANGE;
                 }
             }
         }
         $changes[$path]['changeType'] = $change_type;
     }
     // Go through all the affected directories and identify any which were
     // actually added or deleted.
     $dir_status = array();
     foreach ($maybe_del_directories as $dir) {
         $exists = false;
         foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) {
             if (isset($dir_status[$path])) {
                 break;
             }
             // If we know some child exists, we know this path exists. If we don't
             // know that a child exists, test if this directory still exists.
             if (!$exists) {
                 $exists = $this->mercurialPathExists($repository, $path, $commit->getCommitIdentifier());
             }
             if ($exists) {
                 $dir_status[$path] = DifferentialChangeType::TYPE_CHILD;
             } else {
                 $dir_status[$path] = DifferentialChangeType::TYPE_DELETE;
             }
         }
     }
     list($stdout) = $repository->execxLocalCommand('parents --rev %s --style default', $commit->getCommitIdentifier());
     $parents = ArcanistMercurialParser::parseMercurialLog($stdout);
     $parent = reset($parents);
     if ($parent) {
         // TODO: We should expand this to a full 40-character hash using "hg id".
         $parent = $parent['rev'];
     }
     foreach ($maybe_new_directories as $dir) {
         $exists = false;
         foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) {
             if (isset($dir_status[$path])) {
                 break;
             }
             if (!$exists) {
                 if ($parent) {
                     $exists = $this->mercurialPathExists($repository, $path, $parent);
                 } else {
                     $exists = false;
                 }
             }
             if ($exists) {
                 $dir_status[$path] = DifferentialChangeType::TYPE_CHILD;
             } else {
                 $dir_status[$path] = DifferentialChangeType::TYPE_ADD;
             }
         }
     }
     foreach ($all_directories as $dir) {
         foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) {
             if (isset($dir_status[$path])) {
                 break;
             }
             $dir_status[$path] = DifferentialChangeType::TYPE_CHILD;
         }
     }
     // Merge all the directory statuses into the path statuses.
     foreach ($dir_status as $path => $status) {
         if (isset($changes[$path])) {
             // TODO: The UI probably doesn't handle any of these cases with
             // terrible elegance, but they are exceedingly rare.
             $existing_type = $changes[$path]['changeType'];
             if ($existing_type == DifferentialChangeType::TYPE_DELETE) {
                 // This change removes a file, replaces it with a directory, and then
                 // adds children of that directory. Mark it as a "change" instead,
                 // and make the type a directory.
                 $changes[$path]['fileType'] = DifferentialChangeType::FILE_DIRECTORY;
                 $changes[$path]['changeType'] = DifferentialChangeType::TYPE_CHANGE;
             } else {
                 if ($existing_type == DifferentialChangeType::TYPE_MOVE_AWAY || $existing_type == DifferentialChangeType::TYPE_MULTICOPY) {
                     // This change moves or copies a file, replaces it with a directory,
                     // and then adds children to that directory. Mark it as "copy away"
                     // instead of whatever it was, and make the type a directory.
                     $changes[$path]['fileType'] = DifferentialChangeType::FILE_DIRECTORY;
                     $changes[$path]['changeType'] = DifferentialChangeType::TYPE_COPY_AWAY;
                 } else {
                     if ($existing_type == DifferentialChangeType::TYPE_ADD) {
                         // This change removes a diretory and replaces it with a file. Mark
                         // it as "change" instead of "add".
                         $changes[$path]['changeType'] = DifferentialChangeType::TYPE_CHANGE;
                     }
                 }
             }
             continue;
         }
         $changes[$path] = array('path' => $path, 'isDirect' => $status == DifferentialChangeType::TYPE_CHILD ? false : true, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'changeType' => $status, 'targetPath' => null, 'targetCommitID' => null) + $common_attributes;
     }
     // TODO: use "hg diff --git" to figure out which files are symlinks.
     foreach ($changes as $path => $change) {
         if (empty($change['fileType'])) {
             $changes[$path]['fileType'] = DifferentialChangeType::FILE_NORMAL;
         }
     }
     $all_paths = array();
     foreach ($changes as $path => $change) {
         $all_paths[$path] = true;
         if ($change['targetPath']) {
             $all_paths[$change['targetPath']] = true;
         }
     }
     $path_map = $this->lookupOrCreatePaths(array_keys($all_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));
     }
     $this->finishParse();
 }