public static function renderLastModifiedColumns(PhabricatorRepository $repository, array $handles, PhabricatorRepositoryCommit $commit = null, PhabricatorRepositoryCommitData $data = null)
 {
     if ($commit) {
         $epoch = $commit->getEpoch();
         $modified = DiffusionView::linkCommit($repository, $commit->getCommitIdentifier());
         $date = date('M j, Y', $epoch);
         $time = date('g:i A', $epoch);
     } else {
         $modified = '';
         $date = '';
         $time = '';
     }
     if ($data) {
         $author_phid = $data->getCommitDetail('authorPHID');
         if ($author_phid && isset($handles[$author_phid])) {
             $author = $handles[$author_phid]->renderLink();
         } else {
             $author = phutil_escape_html($data->getAuthorName());
         }
         $details = AphrontTableView::renderSingleDisplayLine(phutil_escape_html($data->getSummary()));
     } else {
         $author = '';
         $details = '';
     }
     return array('commit' => $modified, 'date' => $date, 'time' => $time, 'author' => $author, 'details' => $details);
 }
 public function newDiffFromCommit(PhabricatorRepositoryCommit $commit)
 {
     $viewer = $this->getViewer();
     $repository = $commit->getRepository();
     $identifier = $commit->getCommitIdentifier();
     $monogram = $commit->getMonogram();
     $drequest = DiffusionRequest::newFromDictionary(array('user' => $viewer, 'repository' => $repository));
     $raw_diff = DiffusionQuery::callConduitWithDiffusionRequest($viewer, $drequest, 'diffusion.rawdiffquery', array('commit' => $identifier));
     // TODO: Support adds, deletes and moves under SVN.
     if (strlen($raw_diff)) {
         $changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
     } else {
         // This is an empty diff, maybe made with `git commit --allow-empty`.
         // NOTE: These diffs have the same tree hash as their ancestors, so
         // they may attach to revisions in an unexpected way. Just let this
         // happen for now, although it might make sense to special case it
         // eventually.
         $changes = array();
     }
     $diff = DifferentialDiff::newFromRawChanges($viewer, $changes)->setRepositoryPHID($repository->getPHID())->setCreationMethod('commit')->setSourceControlSystem($repository->getVersionControlSystem())->setLintStatus(DifferentialLintStatus::LINT_AUTO_SKIP)->setUnitStatus(DifferentialUnitStatus::UNIT_AUTO_SKIP)->setDateCreated($commit->getEpoch())->setDescription($monogram);
     $author_phid = $this->getAuthorPHID();
     if ($author_phid !== null) {
         $diff->setAuthorPHID($author_phid);
     }
     $parents = DiffusionQuery::callConduitWithDiffusionRequest($viewer, $drequest, 'diffusion.commitparentsquery', array('commit' => $identifier));
     if ($parents) {
         $diff->setSourceControlBaseRevision(head($parents));
     }
     // TODO: Attach binary files.
     return $diff->save();
 }
 public static function indexCommit(PhabricatorRepositoryCommit $commit)
 {
     $commit_data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID());
     $date_created = $commit->getEpoch();
     $commit_message = $commit_data->getCommitMessage();
     $author_phid = $commit_data->getCommitDetail('authorPHID');
     $repository = id(new PhabricatorRepository())->loadOneWhere('id = %d', $commit->getRepositoryID());
     if (!$repository) {
         return;
     }
     $title = 'r' . $repository->getCallsign() . $commit->getCommitIdentifier() . " " . $commit_data->getSummary();
     $doc = new PhabricatorSearchAbstractDocument();
     $doc->setPHID($commit->getPHID());
     $doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_CMIT);
     $doc->setDocumentCreated($date_created);
     $doc->setDocumentModified($date_created);
     $doc->setDocumentTitle($title);
     $doc->addField(PhabricatorSearchField::FIELD_BODY, $commit_message);
     if ($author_phid) {
         $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, $author_phid, PhabricatorPHIDConstants::PHID_TYPE_USER, $date_created);
     }
     $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY, $repository->getPHID(), PhabricatorPHIDConstants::PHID_TYPE_REPO, $date_created);
     $comments = id(new PhabricatorAuditComment())->loadAllWhere('targetPHID = %s', $commit->getPHID());
     foreach ($comments as $comment) {
         if (strlen($comment->getContent())) {
             $doc->addField(PhabricatorSearchField::FIELD_COMMENT, $comment->getContent());
         }
     }
     self::reindexAbstractDocument($doc);
 }
 private function renderColumns(DiffusionRequest $drequest, array $handles, PhabricatorRepositoryCommit $commit = null, $lint = null)
 {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $viewer = $this->getRequest()->getUser();
     if ($commit) {
         $epoch = $commit->getEpoch();
         $modified = DiffusionView::linkCommit($drequest->getRepository(), $commit->getCommitIdentifier());
         $date = phabricator_date($epoch, $viewer);
         $time = phabricator_time($epoch, $viewer);
     } else {
         $modified = '';
         $date = '';
         $time = '';
     }
     $data = $commit->getCommitData();
     if ($data) {
         $author_phid = $data->getCommitDetail('authorPHID');
         if ($author_phid && isset($handles[$author_phid])) {
             $author = $handles[$author_phid]->renderLink();
         } else {
             $author = DiffusionView::renderName($data->getAuthorName());
         }
         $committer = $data->getCommitDetail('committer');
         if ($committer) {
             $committer_phid = $data->getCommitDetail('committerPHID');
             if ($committer_phid && isset($handles[$committer_phid])) {
                 $committer = $handles[$committer_phid]->renderLink();
             } else {
                 $committer = DiffusionView::renderName($committer);
             }
             if ($author != $committer) {
                 $author = hsprintf('%s/%s', $author, $committer);
             }
         }
         $details = AphrontTableView::renderSingleDisplayLine($data->getSummary());
     } else {
         $author = '';
         $details = '';
     }
     $return = array('commit' => $modified, 'date' => $date, 'time' => $time, 'author' => $author, 'details' => $details);
     if ($lint !== null) {
         $return['lint'] = phutil_tag('a', array('href' => $drequest->generateURI(array('action' => 'lint', 'lint' => null))), number_format($lint));
     }
     // The client treats these results as markup, so make sure they have been
     // escaped correctly.
     foreach ($return as $key => $value) {
         $return[$key] = hsprintf('%s', $value);
     }
     return $return;
 }
 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;
     }
     // Check if the commit has parents. We're testing to see whether it is the
     // first commit in history (in which case we must use "git log") or some
     // other commit (in which case we can use "git diff"). We'd rather use
     // "git diff" because it has the right behavior for merge commits, but
     // it requires the commit to have a parent that we can diff against. The
     // first commit doesn't, so "commit^" is not a valid ref.
     list($parents) = $repository->execxLocalCommand('log -n1 --format=%s %s', '%P', $commit->getCommitIdentifier());
     $use_log = !strlen(trim($parents));
     if ($use_log) {
         // This is the first commit so we need to use "log". We know it's not a
         // merge commit because it couldn't be merging anything, so this is safe.
         // NOTE: "--pretty=format: " is to disable diff output, we only want the
         // part we get from "--raw".
         list($raw) = $repository->execxLocalCommand('log -n1 -M -C -B --find-copies-harder --raw -t ' . '--pretty=format: --abbrev=40 %s', $commit->getCommitIdentifier());
     } else {
         // Otherwise, we can use "diff", which will give us output for merges.
         // We diff against the first parent, as this is generally the expectation
         // and results in sensible behavior.
         list($raw) = $repository->execxLocalCommand('diff -n1 -M -C -B --find-copies-harder --raw -t ' . '--abbrev=40 %s^1 %s', $commit->getCommitIdentifier(), $commit->getCommitIdentifier());
     }
     $changes = array();
     $move_away = array();
     $copy_away = array();
     $lines = explode("\n", $raw);
     foreach ($lines as $line) {
         if (!strlen(trim($line))) {
             continue;
         }
         list($old_mode, $new_mode, $old_hash, $new_hash, $more_stuff) = preg_split('/ +/', $line, 5);
         // We may only have two pieces here.
         list($action, $src_path, $dst_path) = array_merge(explode("\t", $more_stuff), array(null));
         // Normalize the paths for consistency with the SVN workflow.
         $src_path = '/' . $src_path;
         if ($dst_path) {
             $dst_path = '/' . $dst_path;
         }
         $old_mode = intval($old_mode, 8);
         $new_mode = intval($new_mode, 8);
         switch ($new_mode & 0160000) {
             case 0160000:
                 $file_type = DifferentialChangeType::FILE_SUBMODULE;
                 break;
             case 0120000:
                 $file_type = DifferentialChangeType::FILE_SYMLINK;
                 break;
             case 040000:
                 $file_type = DifferentialChangeType::FILE_DIRECTORY;
                 break;
             default:
                 $file_type = DifferentialChangeType::FILE_NORMAL;
                 break;
         }
         // TODO: We can detect binary changes as git does, through a combination
         // of running 'git check-attr' for stuff like 'binary', 'merge' or 'diff',
         // and by falling back to inspecting the first 8,000 characters of the
         // buffer for null bytes (this is seriously git's algorithm, see
         // buffer_is_binary() in xdiff-interface.c).
         $change_type = null;
         $change_path = $src_path;
         $change_target = null;
         $is_direct = true;
         switch ($action[0]) {
             case 'A':
                 $change_type = DifferentialChangeType::TYPE_ADD;
                 break;
             case 'D':
                 $change_type = DifferentialChangeType::TYPE_DELETE;
                 break;
             case 'C':
                 $change_type = DifferentialChangeType::TYPE_COPY_HERE;
                 $change_path = $dst_path;
                 $change_target = $src_path;
                 $copy_away[$change_target][] = $change_path;
                 break;
             case 'R':
                 $change_type = DifferentialChangeType::TYPE_MOVE_HERE;
                 $change_path = $dst_path;
                 $change_target = $src_path;
                 $move_away[$change_target][] = $change_path;
                 break;
             case 'T':
                 // Type of the file changed, fall through and treat it as a
                 // modification. Not 100% sure this is the right thing to do but it
                 // seems reasonable.
             // Type of the file changed, fall through and treat it as a
             // modification. Not 100% sure this is the right thing to do but it
             // seems reasonable.
             case 'M':
                 if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
                     $change_type = DifferentialChangeType::TYPE_CHILD;
                     $is_direct = false;
                 } else {
                     $change_type = DifferentialChangeType::TYPE_CHANGE;
                 }
                 break;
                 // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible
                 // in theory but shouldn't appear here.
             // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible
             // in theory but shouldn't appear here.
             default:
                 throw new Exception("Failed to parse line '{$line}'.");
         }
         $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $file_type, 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null);
     }
     // Add a change to '/' since git doesn't mention it.
     $changes['/'] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => '/', 'changeType' => DifferentialChangeType::TYPE_CHILD, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'isDirect' => false, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null);
     foreach ($copy_away as $change_path => $destinations) {
         if (isset($move_away[$change_path])) {
             $change_type = DifferentialChangeType::TYPE_MULTICOPY;
             $is_direct = true;
             unset($move_away[$change_path]);
         } else {
             $change_type = DifferentialChangeType::TYPE_COPY_AWAY;
             $is_direct = false;
         }
         $reference = $changes[reset($destinations)];
         $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $reference['fileType'], 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null);
     }
     foreach ($move_away as $change_path => $destinations) {
         $reference = $changes[reset($destinations)];
         $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => DifferentialChangeType::TYPE_MOVE_AWAY, 'fileType' => $reference['fileType'], 'isDirect' => true, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null);
     }
     $paths = array();
     foreach ($changes as $change) {
         $paths[$change['path']] = true;
         if ($change['targetPath']) {
             $paths[$change['targetPath']] = true;
         }
     }
     $path_map = $this->lookupOrCreatePaths(array_keys($paths));
     foreach ($changes as $key => $change) {
         $changes[$key]['pathID'] = $path_map[$change['path']];
         if ($change['targetPath']) {
             $changes[$key]['targetPathID'] = $path_map[$change['targetPath']];
         } else {
             $changes[$key]['targetPathID'] = null;
         }
     }
     $conn_w = $repository->establishConnection('w');
     $changes_sql = array();
     foreach ($changes as $change) {
         $values = array((int) $change['repositoryID'], (int) $change['pathID'], (int) $change['commitID'], $change['targetPathID'] ? (int) $change['targetPathID'] : 'null', $change['targetCommitID'] ? (int) $change['targetCommitID'] : 'null', (int) $change['changeType'], (int) $change['fileType'], (int) $change['isDirect'], (int) $change['commitSequence']);
         $changes_sql[] = '(' . implode(', ', $values) . ')';
     }
     queryfx($conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit->getID());
     foreach (array_chunk($changes_sql, 256) as $sql_chunk) {
         queryfx($conn_w, 'INSERT INTO %T
       (repositoryID, pathID, commitID, targetPathID, targetCommitID,
         changeType, fileType, isDirect, commitSequence)
       VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, implode(', ', $sql_chunk));
     }
     $this->finishParse();
 }
 private function loadCommitProperties(PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $parents, array $audit_requests)
 {
     assert_instances_of($parents, 'PhabricatorRepositoryCommit');
     $viewer = $this->getRequest()->getUser();
     $commit_phid = $commit->getPHID();
     $drequest = $this->getDiffusionRequest();
     $repository = $drequest->getRepository();
     $edge_query = id(new PhabricatorEdgeQuery())->withSourcePHIDs(array($commit_phid))->withEdgeTypes(array(DiffusionCommitHasTaskEdgeType::EDGECONST, DiffusionCommitHasRevisionEdgeType::EDGECONST, DiffusionCommitRevertsCommitEdgeType::EDGECONST, DiffusionCommitRevertedByCommitEdgeType::EDGECONST));
     $edges = $edge_query->execute();
     $task_phids = array_keys($edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]);
     $revision_phid = key($edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]);
     $reverts_phids = array_keys($edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]);
     $reverted_by_phids = array_keys($edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]);
     $phids = $edge_query->getDestinationPHIDs(array($commit_phid));
     if ($data->getCommitDetail('authorPHID')) {
         $phids[] = $data->getCommitDetail('authorPHID');
     }
     if ($data->getCommitDetail('reviewerPHID')) {
         $phids[] = $data->getCommitDetail('reviewerPHID');
     }
     if ($data->getCommitDetail('committerPHID')) {
         $phids[] = $data->getCommitDetail('committerPHID');
     }
     if ($parents) {
         foreach ($parents as $parent) {
             $phids[] = $parent->getPHID();
         }
     }
     // NOTE: We should never normally have more than a single push log, but
     // it can occur naturally if a commit is pushed, then the branch it was
     // on is deleted, then the commit is pushed again (or through other similar
     // chains of events). This should be rare, but does not indicate a bug
     // or data issue.
     // NOTE: We never query push logs in SVN because the commiter is always
     // the pusher and the commit time is always the push time; the push log
     // is redundant and we save a query by skipping it.
     $push_logs = array();
     if ($repository->isHosted() && !$repository->isSVN()) {
         $push_logs = id(new PhabricatorRepositoryPushLogQuery())->setViewer($viewer)->withRepositoryPHIDs(array($repository->getPHID()))->withNewRefs(array($commit->getCommitIdentifier()))->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT))->execute();
         foreach ($push_logs as $log) {
             $phids[] = $log->getPusherPHID();
         }
     }
     $handles = array();
     if ($phids) {
         $handles = $this->loadViewerHandles($phids);
     }
     $props = array();
     if ($commit->getAuditStatus()) {
         $status = PhabricatorAuditCommitStatusConstants::getStatusName($commit->getAuditStatus());
         $tag = id(new PHUITagView())->setType(PHUITagView::TYPE_STATE)->setName($status);
         switch ($commit->getAuditStatus()) {
             case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT:
                 $tag->setBackgroundColor(PHUITagView::COLOR_ORANGE);
                 break;
             case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED:
                 $tag->setBackgroundColor(PHUITagView::COLOR_RED);
                 break;
             case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED:
                 $tag->setBackgroundColor(PHUITagView::COLOR_BLUE);
                 break;
             case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED:
                 $tag->setBackgroundColor(PHUITagView::COLOR_GREEN);
                 break;
         }
         $props['Status'] = $tag;
     }
     if ($audit_requests) {
         $user_requests = array();
         $other_requests = array();
         foreach ($audit_requests as $audit_request) {
             if ($audit_request->isUser()) {
                 $user_requests[] = $audit_request;
             } else {
                 $other_requests[] = $audit_request;
             }
         }
         if ($user_requests) {
             $props['Auditors'] = $this->renderAuditStatusView($user_requests);
         }
         if ($other_requests) {
             $props['Project/Package Auditors'] = $this->renderAuditStatusView($other_requests);
         }
     }
     $author_phid = $data->getCommitDetail('authorPHID');
     $author_name = $data->getAuthorName();
     if (!$repository->isSVN()) {
         $authored_info = id(new PHUIStatusItemView());
         // TODO: In Git, a distinct authorship date is available. When present,
         // we should show it here.
         if ($author_phid) {
             $authored_info->setTarget($handles[$author_phid]->renderLink());
         } else {
             if (strlen($author_name)) {
                 $authored_info->setTarget($author_name);
             }
         }
         $props['Authored'] = id(new PHUIStatusListView())->addItem($authored_info);
     }
     $committed_info = id(new PHUIStatusItemView())->setNote(phabricator_datetime($commit->getEpoch(), $viewer));
     $committer_phid = $data->getCommitDetail('committerPHID');
     $committer_name = $data->getCommitDetail('committer');
     if ($committer_phid) {
         $committed_info->setTarget($handles[$committer_phid]->renderLink());
     } else {
         if (strlen($committer_name)) {
             $committed_info->setTarget($committer_name);
         } else {
             if ($author_phid) {
                 $committed_info->setTarget($handles[$author_phid]->renderLink());
             } else {
                 if (strlen($author_name)) {
                     $committed_info->setTarget($author_name);
                 }
             }
         }
     }
     $props['Committed'] = id(new PHUIStatusListView())->addItem($committed_info);
     if ($push_logs) {
         $pushed_list = new PHUIStatusListView();
         foreach ($push_logs as $push_log) {
             $pushed_item = id(new PHUIStatusItemView())->setTarget($handles[$push_log->getPusherPHID()]->renderLink())->setNote(phabricator_datetime($push_log->getEpoch(), $viewer));
             $pushed_list->addItem($pushed_item);
         }
         $props['Pushed'] = $pushed_list;
     }
     $reviewer_phid = $data->getCommitDetail('reviewerPHID');
     if ($reviewer_phid) {
         $props['Reviewer'] = $handles[$reviewer_phid]->renderLink();
     }
     if ($revision_phid) {
         $props['Differential Revision'] = $handles[$revision_phid]->renderLink();
     }
     if ($parents) {
         $parent_links = array();
         foreach ($parents as $parent) {
             $parent_links[] = $handles[$parent->getPHID()]->renderLink();
         }
         $props['Parents'] = phutil_implode_html(" · ", $parent_links);
     }
     $props['Branches'] = phutil_tag('span', array('id' => 'commit-branches'), pht('Unknown'));
     $props['Tags'] = phutil_tag('span', array('id' => 'commit-tags'), pht('Unknown'));
     $callsign = $repository->getCallsign();
     $root = '/diffusion/' . $callsign . '/commit/' . $commit->getCommitIdentifier();
     Javelin::initBehavior('diffusion-commit-branches', array($root . '/branches/' => 'commit-branches', $root . '/tags/' => 'commit-tags'));
     $refs = $this->buildRefs($drequest);
     if ($refs) {
         $props['References'] = $refs;
     }
     if ($reverts_phids) {
         $props[pht('Reverts')] = $viewer->renderHandleList($reverts_phids);
     }
     if ($reverted_by_phids) {
         $props[pht('Reverted By')] = $viewer->renderHandleList($reverted_by_phids);
     }
     if ($task_phids) {
         $task_list = array();
         foreach ($task_phids as $phid) {
             $task_list[] = $handles[$phid]->renderLink();
         }
         $task_list = phutil_implode_html(phutil_tag('br'), $task_list);
         $props['Tasks'] = $task_list;
     }
     return $props;
 }
 private function getCommitProperties(PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $parents)
 {
     assert_instances_of($parents, 'PhabricatorRepositoryCommit');
     $user = $this->getRequest()->getUser();
     $task_phids = PhabricatorEdgeQuery::loadDestinationPHIDs($commit->getPHID(), PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK);
     $phids = $task_phids;
     if ($data->getCommitDetail('authorPHID')) {
         $phids[] = $data->getCommitDetail('authorPHID');
     }
     if ($data->getCommitDetail('reviewerPHID')) {
         $phids[] = $data->getCommitDetail('reviewerPHID');
     }
     if ($data->getCommitDetail('differential.revisionPHID')) {
         $phids[] = $data->getCommitDetail('differential.revisionPHID');
     }
     if ($parents) {
         foreach ($parents as $parent) {
             $phids[] = $parent->getPHID();
         }
     }
     $handles = array();
     if ($phids) {
         $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     }
     $props = array();
     if ($commit->getAuditStatus()) {
         $status = PhabricatorAuditCommitStatusConstants::getStatusName($commit->getAuditStatus());
         $props['Status'] = phutil_render_tag('strong', array(), phutil_escape_html($status));
     }
     $props['Committed'] = phabricator_datetime($commit->getEpoch(), $user);
     $author_phid = $data->getCommitDetail('authorPHID');
     if ($data->getCommitDetail('authorPHID')) {
         $props['Author'] = $handles[$author_phid]->renderLink();
     } else {
         $props['Author'] = phutil_escape_html($data->getAuthorName());
     }
     $reviewer_phid = $data->getCommitDetail('reviewerPHID');
     $reviewer_name = $data->getCommitDetail('reviewerName');
     if ($reviewer_phid) {
         $props['Reviewer'] = $handles[$reviewer_phid]->renderLink();
     } else {
         if ($reviewer_name) {
             $props['Reviewer'] = phutil_escape_html($reviewer_name);
         }
     }
     $revision_phid = $data->getCommitDetail('differential.revisionPHID');
     if ($revision_phid) {
         $props['Differential Revision'] = $handles[$revision_phid]->renderLink();
     }
     if ($parents) {
         $parent_links = array();
         foreach ($parents as $parent) {
             $parent_links[] = $handles[$parent->getPHID()]->renderLink();
         }
         $props['Parents'] = implode(' · ', $parent_links);
     }
     $request = $this->getDiffusionRequest();
     $contains = DiffusionContainsQuery::newFromDiffusionRequest($request);
     $branches = $contains->loadContainingBranches();
     if ($branches) {
         // TODO: Separate these into 'tracked' and other; link tracked branches.
         $branches = implode(', ', array_keys($branches));
         $branches = phutil_escape_html($branches);
         $props['Branches'] = $branches;
     }
     if ($task_phids) {
         $task_list = array();
         foreach ($task_phids as $phid) {
             $task_list[] = $handles[$phid]->renderLink();
         }
         $task_list = implode('<br />', $task_list);
         $props['Tasks'] = $task_list;
     }
     return $props;
 }
 private function renderCommitTooltip(PhabricatorRepositoryCommit $commit, $author)
 {
     $viewer = $this->getRequest()->getUser();
     $date = phabricator_date($commit->getEpoch(), $viewer);
     $summary = trim($commit->getSummary());
     return "{$summary}\n{$date} · {$author}";
 }
 private function renderCommitTooltip(PhabricatorRepositoryCommit $commit, array $handles, $author)
 {
     $viewer = $this->getRequest()->getUser();
     $date = phabricator_date($commit->getEpoch(), $viewer);
     $summary = trim($commit->getSummary());
     if ($commit->getAuthorPHID()) {
         $author = $handles[$commit->getAuthorPHID()]->getName();
     }
     return "{$summary}\n{$date} · {$author}";
 }
 private function publishFeedStory(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data)
 {
     if (time() > $commit->getEpoch() + 24 * 60 * 60) {
         // Don't publish stories that are more than 24 hours old, to avoid
         // ridiculous levels of feed spam if a repository is imported without
         // disabling feed publishing.
         return;
     }
     $author_phid = $commit->getAuthorPHID();
     $committer_phid = $data->getCommitDetail('committerPHID');
     $publisher = new PhabricatorFeedStoryPublisher();
     $publisher->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_COMMIT);
     $publisher->setStoryData(array('commitPHID' => $commit->getPHID(), 'summary' => $data->getSummary(), 'authorName' => $data->getAuthorName(), 'authorPHID' => $author_phid, 'committerName' => $data->getCommitDetail('committer'), 'committerPHID' => $committer_phid));
     $publisher->setStoryTime($commit->getEpoch());
     $publisher->setRelatedPHIDs(array_filter(array($author_phid, $committer_phid)));
     if ($author_phid) {
         $publisher->setStoryAuthorPHID($author_phid);
     }
     $publisher->publish();
 }
 protected function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit)
 {
     $full_name = 'r' . $repository->getCallsign() . $commit->getCommitIdentifier();
     echo "Parsing {$full_name}...\n";
     if ($this->isBadCommit($full_name)) {
         echo "This commit is marked bad!\n";
         return;
     }
     $local_path = $repository->getDetail('local-path');
     list($raw) = execx('(cd %s && git log -n1 -M -C -B --find-copies-harder --raw -t ' . '--abbrev=40 --pretty=format: %s)', $local_path, $commit->getCommitIdentifier());
     $changes = array();
     $move_away = array();
     $copy_away = array();
     $lines = explode("\n", $raw);
     foreach ($lines as $line) {
         if (!strlen(trim($line))) {
             continue;
         }
         list($old_mode, $new_mode, $old_hash, $new_hash, $more_stuff) = preg_split('/ +/', $line);
         // We may only have two pieces here.
         list($action, $src_path, $dst_path) = array_merge(explode("\t", $more_stuff), array(null));
         // Normalize the paths for consistency with the SVN workflow.
         $src_path = '/' . $src_path;
         if ($dst_path) {
             $dst_path = '/' . $dst_path;
         }
         $old_mode = intval($old_mode, 8);
         $new_mode = intval($new_mode, 8);
         $file_type = DifferentialChangeType::FILE_NORMAL;
         if ($new_mode & 040000) {
             $file_type = DifferentialChangeType::FILE_DIRECTORY;
         } else {
             if ($new_mode & 0120000) {
                 $file_type = DifferentialChangeType::FILE_SYMLINK;
             }
         }
         // TODO: We can detect binary changes as git does, through a combination
         // of running 'git check-attr' for stuff like 'binary', 'merge' or 'diff',
         // and by falling back to inspecting the first 8,000 characters of the
         // buffer for null bytes (this is seriously git's algorithm, see
         // buffer_is_binary() in xdiff-interface.c).
         $change_type = null;
         $change_path = $src_path;
         $change_target = null;
         $is_direct = true;
         switch ($action[0]) {
             case 'A':
                 $change_type = DifferentialChangeType::TYPE_ADD;
                 break;
             case 'D':
                 $change_type = DifferentialChangeType::TYPE_DELETE;
                 break;
             case 'C':
                 $change_type = DifferentialChangeType::TYPE_COPY_HERE;
                 $change_path = $dst_path;
                 $change_target = $src_path;
                 $copy_away[$change_target][] = $change_path;
                 break;
             case 'R':
                 $change_type = DifferentialChangeType::TYPE_MOVE_HERE;
                 $change_path = $dst_path;
                 $change_target = $src_path;
                 $move_away[$change_target][] = $change_path;
                 break;
             case 'T':
                 // Type of the file changed, fall through and treat it as a
                 // modification. Not 100% sure this is the right thing to do but it
                 // seems reasonable.
             // Type of the file changed, fall through and treat it as a
             // modification. Not 100% sure this is the right thing to do but it
             // seems reasonable.
             case 'M':
                 if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
                     $change_type = DifferentialChangeType::TYPE_CHILD;
                     $is_direct = false;
                 } else {
                     $change_type = DifferentialChangeType::TYPE_CHANGE;
                 }
                 break;
                 // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible
                 // in theory but shouldn't appear here.
             // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible
             // in theory but shouldn't appear here.
             default:
                 throw new Exception("Failed to parse line '{$line}'.");
         }
         $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $file_type, 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null);
     }
     // Add a change to '/' since git doesn't mention it.
     $changes['/'] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => '/', 'changeType' => DifferentialChangeType::TYPE_CHILD, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'isDirect' => false, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null);
     foreach ($copy_away as $change_path => $destinations) {
         if (isset($move_away[$change_path])) {
             $change_type = DifferentialChangeType::TYPE_MULTICOPY;
             $is_direct = true;
             unset($move_away[$change_path]);
         } else {
             $change_type = DifferentialChangeType::TYPE_COPY_AWAY;
             $is_direct = false;
         }
         $reference = $changes[reset($destinations)];
         $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $reference['fileType'], 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null);
     }
     foreach ($move_away as $change_path => $destinations) {
         $reference = $changes[reset($destinations)];
         $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => DifferentialChangeType::TYPE_MOVE_AWAY, 'fileType' => $reference['fileType'], 'isDirect' => true, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null);
     }
     $paths = array();
     foreach ($changes as $change) {
         $paths[$change['path']] = true;
         if ($change['targetPath']) {
             $paths[$change['targetPath']] = true;
         }
     }
     $path_map = $this->lookupOrCreatePaths(array_keys($paths));
     foreach ($changes as $key => $change) {
         $changes[$key]['pathID'] = $path_map[$change['path']];
         if ($change['targetPath']) {
             $changes[$key]['targetPathID'] = $path_map[$change['targetPath']];
         } else {
             $changes[$key]['targetPathID'] = null;
         }
     }
     $conn_w = $repository->establishConnection('w');
     $changes_sql = array();
     foreach ($changes as $change) {
         $values = array((int) $change['repositoryID'], (int) $change['pathID'], (int) $change['commitID'], $change['targetPathID'] ? (int) $change['targetPathID'] : 'null', $change['targetCommitID'] ? (int) $change['targetCommitID'] : 'null', (int) $change['changeType'], (int) $change['fileType'], (int) $change['isDirect'], (int) $change['commitSequence']);
         $changes_sql[] = '(' . implode(', ', $values) . ')';
     }
     queryfx($conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit->getID());
     foreach (array_chunk($changes_sql, 256) as $sql_chunk) {
         queryfx($conn_w, 'INSERT INTO %T
       (repositoryID, pathID, commitID, targetPathID, targetCommitID,
         changeType, fileType, isDirect, commitSequence)
       VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, implode(', ', $sql_chunk));
     }
     $this->finishParse();
 }
 private function getCommitProperties(PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $parents)
 {
     assert_instances_of($parents, 'PhabricatorRepositoryCommit');
     $user = $this->getRequest()->getUser();
     $commit_phid = $commit->getPHID();
     $edges = id(new PhabricatorEdgeQuery())->withSourcePHIDs(array($commit_phid))->withEdgeTypes(array(PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK, PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT))->execute();
     $task_phids = array_keys($edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK]);
     $proj_phids = array_keys($edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT]);
     $phids = array_merge($task_phids, $proj_phids);
     if ($data->getCommitDetail('authorPHID')) {
         $phids[] = $data->getCommitDetail('authorPHID');
     }
     if ($data->getCommitDetail('reviewerPHID')) {
         $phids[] = $data->getCommitDetail('reviewerPHID');
     }
     if ($data->getCommitDetail('committerPHID')) {
         $phids[] = $data->getCommitDetail('committerPHID');
     }
     if ($data->getCommitDetail('differential.revisionPHID')) {
         $phids[] = $data->getCommitDetail('differential.revisionPHID');
     }
     if ($parents) {
         foreach ($parents as $parent) {
             $phids[] = $parent->getPHID();
         }
     }
     $handles = array();
     if ($phids) {
         $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     }
     $props = array();
     if ($commit->getAuditStatus()) {
         $status = PhabricatorAuditCommitStatusConstants::getStatusName($commit->getAuditStatus());
         $props['Status'] = phutil_render_tag('strong', array(), phutil_escape_html($status));
     }
     $props['Committed'] = phabricator_datetime($commit->getEpoch(), $user);
     $author_phid = $data->getCommitDetail('authorPHID');
     if ($data->getCommitDetail('authorPHID')) {
         $props['Author'] = $handles[$author_phid]->renderLink();
     } else {
         $props['Author'] = phutil_escape_html($data->getAuthorName());
     }
     $reviewer_phid = $data->getCommitDetail('reviewerPHID');
     $reviewer_name = $data->getCommitDetail('reviewerName');
     if ($reviewer_phid) {
         $props['Reviewer'] = $handles[$reviewer_phid]->renderLink();
     } else {
         if ($reviewer_name) {
             $props['Reviewer'] = phutil_escape_html($reviewer_name);
         }
     }
     $committer = $data->getCommitDetail('committer');
     if ($committer) {
         $committer_phid = $data->getCommitDetail('committerPHID');
         if ($data->getCommitDetail('committerPHID')) {
             $props['Committer'] = $handles[$committer_phid]->renderLink();
         } else {
             $props['Committer'] = phutil_escape_html($committer);
         }
     }
     $revision_phid = $data->getCommitDetail('differential.revisionPHID');
     if ($revision_phid) {
         $props['Differential Revision'] = $handles[$revision_phid]->renderLink();
     }
     if ($parents) {
         $parent_links = array();
         foreach ($parents as $parent) {
             $parent_links[] = $handles[$parent->getPHID()]->renderLink();
         }
         $props['Parents'] = implode(' &middot; ', $parent_links);
     }
     $request = $this->getDiffusionRequest();
     $props['Branches'] = '<span id="commit-branches">Unknown</span>';
     $props['Tags'] = '<span id="commit-tags">Unknown</span>';
     $callsign = $request->getRepository()->getCallsign();
     $root = '/diffusion/' . $callsign . '/commit/' . $commit->getCommitIdentifier();
     Javelin::initBehavior('diffusion-commit-branches', array($root . '/branches/' => 'commit-branches', $root . '/tags/' => 'commit-tags'));
     $refs = $this->buildRefs($request);
     if ($refs) {
         $props['References'] = $refs;
     }
     if ($task_phids) {
         $task_list = array();
         foreach ($task_phids as $phid) {
             $task_list[] = $handles[$phid]->renderLink();
         }
         $task_list = implode('<br />', $task_list);
         $props['Tasks'] = $task_list;
     }
     if ($proj_phids) {
         $proj_list = array();
         foreach ($proj_phids as $phid) {
             $proj_list[] = $handles[$phid]->renderLink();
         }
         $proj_list = implode('<br />', $proj_list);
         $props['Projects'] = $proj_list;
     }
     return $props;
 }
 private function buildPropertyListView(PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $audit_requests)
 {
     $viewer = $this->getViewer();
     $commit_phid = $commit->getPHID();
     $drequest = $this->getDiffusionRequest();
     $repository = $drequest->getRepository();
     $view = id(new PHUIPropertyListView())->setUser($this->getRequest()->getUser());
     $edge_query = id(new PhabricatorEdgeQuery())->withSourcePHIDs(array($commit_phid))->withEdgeTypes(array(DiffusionCommitHasTaskEdgeType::EDGECONST, DiffusionCommitHasRevisionEdgeType::EDGECONST, DiffusionCommitRevertsCommitEdgeType::EDGECONST, DiffusionCommitRevertedByCommitEdgeType::EDGECONST));
     $edges = $edge_query->execute();
     $task_phids = array_keys($edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]);
     $revision_phid = key($edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]);
     $reverts_phids = array_keys($edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]);
     $reverted_by_phids = array_keys($edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]);
     $phids = $edge_query->getDestinationPHIDs(array($commit_phid));
     if ($data->getCommitDetail('authorPHID')) {
         $phids[] = $data->getCommitDetail('authorPHID');
     }
     if ($data->getCommitDetail('reviewerPHID')) {
         $phids[] = $data->getCommitDetail('reviewerPHID');
     }
     if ($data->getCommitDetail('committerPHID')) {
         $phids[] = $data->getCommitDetail('committerPHID');
     }
     // NOTE: We should never normally have more than a single push log, but
     // it can occur naturally if a commit is pushed, then the branch it was
     // on is deleted, then the commit is pushed again (or through other similar
     // chains of events). This should be rare, but does not indicate a bug
     // or data issue.
     // NOTE: We never query push logs in SVN because the commiter is always
     // the pusher and the commit time is always the push time; the push log
     // is redundant and we save a query by skipping it.
     $push_logs = array();
     if ($repository->isHosted() && !$repository->isSVN()) {
         $push_logs = id(new PhabricatorRepositoryPushLogQuery())->setViewer($viewer)->withRepositoryPHIDs(array($repository->getPHID()))->withNewRefs(array($commit->getCommitIdentifier()))->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT))->execute();
         foreach ($push_logs as $log) {
             $phids[] = $log->getPusherPHID();
         }
     }
     $handles = array();
     if ($phids) {
         $handles = $this->loadViewerHandles($phids);
     }
     $props = array();
     if ($audit_requests) {
         $user_requests = array();
         $other_requests = array();
         foreach ($audit_requests as $audit_request) {
             if (!$audit_request->isInteresting()) {
                 continue;
             }
             if ($audit_request->isUser()) {
                 $user_requests[] = $audit_request;
             } else {
                 $other_requests[] = $audit_request;
             }
         }
         if ($user_requests) {
             $view->addProperty(pht('Auditors'), $this->renderAuditStatusView($user_requests));
         }
         if ($other_requests) {
             $view->addProperty(pht('Group Auditors'), $this->renderAuditStatusView($other_requests));
         }
     }
     $author_phid = $data->getCommitDetail('authorPHID');
     $author_name = $data->getAuthorName();
     $author_epoch = $data->getCommitDetail('authorEpoch');
     $committed_info = id(new PHUIStatusItemView())->setNote(phabricator_datetime($commit->getEpoch(), $viewer));
     $committer_phid = $data->getCommitDetail('committerPHID');
     $committer_name = $data->getCommitDetail('committer');
     if ($committer_phid) {
         $committed_info->setTarget($handles[$committer_phid]->renderLink());
     } else {
         if (strlen($committer_name)) {
             $committed_info->setTarget($committer_name);
         } else {
             if ($author_phid) {
                 $committed_info->setTarget($handles[$author_phid]->renderLink());
             } else {
                 if (strlen($author_name)) {
                     $committed_info->setTarget($author_name);
                 }
             }
         }
     }
     $committed_list = new PHUIStatusListView();
     $committed_list->addItem($committed_info);
     $view->addProperty(pht('Committed'), $committed_list);
     if ($push_logs) {
         $pushed_list = new PHUIStatusListView();
         foreach ($push_logs as $push_log) {
             $pushed_item = id(new PHUIStatusItemView())->setTarget($handles[$push_log->getPusherPHID()]->renderLink())->setNote(phabricator_datetime($push_log->getEpoch(), $viewer));
             $pushed_list->addItem($pushed_item);
         }
         $view->addProperty(pht('Pushed'), $pushed_list);
     }
     $reviewer_phid = $data->getCommitDetail('reviewerPHID');
     if ($reviewer_phid) {
         $view->addProperty(pht('Reviewer'), $handles[$reviewer_phid]->renderLink());
     }
     if ($revision_phid) {
         $view->addProperty(pht('Differential Revision'), $handles[$revision_phid]->renderLink());
     }
     $parents = $this->getCommitParents();
     if ($parents) {
         $view->addProperty(pht('Parents'), $viewer->renderHandleList(mpull($parents, 'getPHID')));
     }
     if ($this->getCommitExists()) {
         $view->addProperty(pht('Branches'), phutil_tag('span', array('id' => 'commit-branches'), pht('Unknown')));
         $view->addProperty(pht('Tags'), phutil_tag('span', array('id' => 'commit-tags'), pht('Unknown')));
         $identifier = $commit->getCommitIdentifier();
         $root = $repository->getPathURI("commit/{$identifier}");
         Javelin::initBehavior('diffusion-commit-branches', array($root . '/branches/' => 'commit-branches', $root . '/tags/' => 'commit-tags'));
     }
     $refs = $this->getCommitRefs();
     if ($refs) {
         $ref_links = array();
         foreach ($refs as $ref_data) {
             $ref_links[] = phutil_tag('a', array('href' => $ref_data['href']), $ref_data['ref']);
         }
         $view->addProperty(pht('References'), phutil_implode_html(', ', $ref_links));
     }
     if ($reverts_phids) {
         $view->addProperty(pht('Reverts'), $viewer->renderHandleList($reverts_phids));
     }
     if ($reverted_by_phids) {
         $view->addProperty(pht('Reverted By'), $viewer->renderHandleList($reverted_by_phids));
     }
     if ($task_phids) {
         $task_list = array();
         foreach ($task_phids as $phid) {
             $task_list[] = $handles[$phid]->renderLink();
         }
         $task_list = phutil_implode_html(phutil_tag('br'), $task_list);
         $view->addProperty(pht('Tasks'), $task_list);
     }
     return $view;
 }
 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 parseCommitChanges(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit)
 {
     $viewer = PhabricatorUser::getOmnipotentUser();
     $raw = DiffusionQuery::callConduitWithDiffusionRequest($viewer, DiffusionRequest::newFromDictionary(array('repository' => $repository, 'user' => $viewer)), 'diffusion.internal.gitrawdiffquery', array('commit' => $commit->getCommitIdentifier()));
     $changes = array();
     $move_away = array();
     $copy_away = array();
     $lines = explode("\n", $raw);
     foreach ($lines as $line) {
         if (!strlen(trim($line))) {
             continue;
         }
         list($old_mode, $new_mode, $old_hash, $new_hash, $more_stuff) = preg_split('/ +/', $line, 5);
         // We may only have two pieces here.
         list($action, $src_path, $dst_path) = array_merge(explode("\t", $more_stuff), array(null));
         // Normalize the paths for consistency with the SVN workflow.
         $src_path = '/' . $src_path;
         if ($dst_path) {
             $dst_path = '/' . $dst_path;
         }
         $old_mode = intval($old_mode, 8);
         $new_mode = intval($new_mode, 8);
         switch ($new_mode & 0160000) {
             case 0160000:
                 $file_type = DifferentialChangeType::FILE_SUBMODULE;
                 break;
             case 0120000:
                 $file_type = DifferentialChangeType::FILE_SYMLINK;
                 break;
             case 040000:
                 $file_type = DifferentialChangeType::FILE_DIRECTORY;
                 break;
             default:
                 $file_type = DifferentialChangeType::FILE_NORMAL;
                 break;
         }
         // TODO: We can detect binary changes as git does, through a combination
         // of running 'git check-attr' for stuff like 'binary', 'merge' or 'diff',
         // and by falling back to inspecting the first 8,000 characters of the
         // buffer for null bytes (this is seriously git's algorithm, see
         // buffer_is_binary() in xdiff-interface.c).
         $change_type = null;
         $change_path = $src_path;
         $change_target = null;
         $is_direct = true;
         switch ($action[0]) {
             case 'A':
                 $change_type = DifferentialChangeType::TYPE_ADD;
                 break;
             case 'D':
                 $change_type = DifferentialChangeType::TYPE_DELETE;
                 break;
             case 'C':
                 $change_type = DifferentialChangeType::TYPE_COPY_HERE;
                 $change_path = $dst_path;
                 $change_target = $src_path;
                 $copy_away[$change_target][] = $change_path;
                 break;
             case 'R':
                 $change_type = DifferentialChangeType::TYPE_MOVE_HERE;
                 $change_path = $dst_path;
                 $change_target = $src_path;
                 $move_away[$change_target][] = $change_path;
                 break;
             case 'T':
                 // Type of the file changed, fall through and treat it as a
                 // modification. Not 100% sure this is the right thing to do but it
                 // seems reasonable.
             // Type of the file changed, fall through and treat it as a
             // modification. Not 100% sure this is the right thing to do but it
             // seems reasonable.
             case 'M':
                 if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
                     $change_type = DifferentialChangeType::TYPE_CHILD;
                     $is_direct = false;
                 } else {
                     $change_type = DifferentialChangeType::TYPE_CHANGE;
                 }
                 break;
                 // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible
                 // in theory but shouldn't appear here.
             // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible
             // in theory but shouldn't appear here.
             default:
                 throw new Exception(pht("Failed to parse line '%s'.", $line));
         }
         $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $file_type, 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null);
     }
     // Add a change to '/' since git doesn't mention it.
     $changes['/'] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => '/', 'changeType' => DifferentialChangeType::TYPE_CHILD, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'isDirect' => false, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null);
     foreach ($copy_away as $change_path => $destinations) {
         if (isset($move_away[$change_path])) {
             $change_type = DifferentialChangeType::TYPE_MULTICOPY;
             $is_direct = true;
             unset($move_away[$change_path]);
         } else {
             $change_type = DifferentialChangeType::TYPE_COPY_AWAY;
             // This change is direct if we picked up a modification above (i.e.,
             // the original copy source was also edited). Otherwise the original
             // wasn't touched, so leave it as an indirect change.
             $is_direct = isset($changes[$change_path]);
         }
         $reference = $changes[reset($destinations)];
         $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $reference['fileType'], 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null);
     }
     foreach ($move_away as $change_path => $destinations) {
         $reference = $changes[reset($destinations)];
         $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => DifferentialChangeType::TYPE_MOVE_AWAY, 'fileType' => $reference['fileType'], 'isDirect' => true, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null);
     }
     $paths = array();
     foreach ($changes as $change) {
         $paths[$change['path']] = true;
         if ($change['targetPath']) {
             $paths[$change['targetPath']] = true;
         }
     }
     $path_map = $this->lookupOrCreatePaths(array_keys($paths));
     foreach ($changes as $key => $change) {
         $changes[$key]['pathID'] = $path_map[$change['path']];
         if ($change['targetPath']) {
             $changes[$key]['targetPathID'] = $path_map[$change['targetPath']];
         } else {
             $changes[$key]['targetPathID'] = null;
         }
     }
     $results = array();
     foreach ($changes as $change) {
         $result = id(new PhabricatorRepositoryParsedChange())->setPathID($change['pathID'])->setTargetPathID($change['targetPathID'])->setTargetCommitID($change['targetCommitID'])->setChangeType($change['changeType'])->setFileType($change['fileType'])->setIsDirect($change['isDirect'])->setCommitSequence($change['commitSequence']);
         $results[] = $result;
     }
     return $results;
 }