protected function executeQuery()
 {
     $drequest = $this->getRequest();
     $repository = $drequest->getRepository();
     list($stdout) = $repository->execxLocalCommand('branches');
     $branch_info = ArcanistMercurialParser::parseMercurialBranches($stdout);
     $branches = array();
     foreach ($branch_info as $name => $info) {
         $branch = new DiffusionBranchInformation();
         $branch->setName($name);
         $branch->setHeadCommitIdentifier($info['rev']);
         $branches[] = $branch;
     }
     return $branches;
 }
 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;
         }
     }
 }
 public function getWorkingCopyStatus()
 {
     if (!isset($this->status)) {
         // A reviewable revision spans multiple local commits in Mercurial, but
         // there is no way to get file change status across multiple commits, so
         // just take the entire diff and parse it to figure out what's changed.
         $diff = $this->getFullMercurialDiff();
         if (!$diff) {
             $this->status = array();
             return $this->status;
         }
         $parser = new ArcanistDiffParser();
         $changes = $parser->parseDiff($diff);
         $status_map = array();
         foreach ($changes as $change) {
             $flags = 0;
             switch ($change->getType()) {
                 case ArcanistDiffChangeType::TYPE_ADD:
                 case ArcanistDiffChangeType::TYPE_MOVE_HERE:
                 case ArcanistDiffChangeType::TYPE_COPY_HERE:
                     $flags |= self::FLAG_ADDED;
                     break;
                 case ArcanistDiffChangeType::TYPE_CHANGE:
                 case ArcanistDiffChangeType::TYPE_COPY_AWAY:
                     // Check for changes?
                     $flags |= self::FLAG_MODIFIED;
                     break;
                 case ArcanistDiffChangeType::TYPE_DELETE:
                 case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
                 case ArcanistDiffChangeType::TYPE_MULTICOPY:
                     $flags |= self::FLAG_DELETED;
                     break;
             }
             $status_map[$change->getCurrentPath()] = $flags;
         }
         list($stdout) = $this->execxLocal('status');
         $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
         foreach ($working_status as $path => $status) {
             if ($status & ArcanistRepositoryAPI::FLAG_UNTRACKED) {
                 // If the file is untracked, don't mark it uncommitted.
                 continue;
             }
             $status |= self::FLAG_UNCOMMITTED;
             if (!empty($status_map[$path])) {
                 $status_map[$path] |= $status;
             } else {
                 $status_map[$path] = $status;
             }
         }
         $this->status = $status_map;
     }
     return $this->status;
 }
 public function getBranches()
 {
     list($stdout) = $this->execxLocal('--debug branches');
     $lines = ArcanistMercurialParser::parseMercurialBranches($stdout);
     $branches = array();
     foreach ($lines as $name => $spec) {
         $branches[] = array('name' => $name, 'revision' => $spec['rev']);
     }
     return $branches;
 }
 private function executeHgDiscover(PhabricatorRepository $repository)
 {
     // NOTE: "--debug" gives us 40-character hashes.
     list($stdout) = $repository->execxLocalCommand('--debug branches');
     $branches = ArcanistMercurialParser::parseMercurialBranches($stdout);
     $got_something = false;
     foreach ($branches as $name => $branch) {
         $commit = $branch['rev'];
         if ($this->isKnownCommit($repository, $commit)) {
             continue;
         } else {
             $this->executeHgDiscoverCommit($repository, $commit);
             $got_something = true;
         }
     }
     return $got_something;
 }
 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();
 }
 protected function buildUncommittedStatus()
 {
     list($stdout) = $this->execxLocal('status');
     $results = new PhutilArrayWithDefaultValue();
     $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
     foreach ($working_status as $path => $mask) {
         if (!($mask & ArcanistRepositoryAPI::FLAG_UNTRACKED)) {
             // Mark tracked files as uncommitted.
             $mask |= self::FLAG_UNCOMMITTED;
         }
         $results[$path] |= $mask;
     }
     return $results->toArray();
 }