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));
     }
 }
 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();
 }