diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -52,8 +52,12 @@ } public function getGitVersion() { - list($stdout) = $this->execxLocal('--version'); - return rtrim(str_replace('git version ', '', $stdout)); + static $version = null; + if ($version === null) { + list($stdout) = $this->execxLocal('--version'); + $version = rtrim(str_replace('git version ', '', $stdout)); + } + return $version; } public function getMetadataPath() { @@ -645,8 +649,70 @@ return $this->executeSVNFindRev($hash, 'SVN'); } + private function buildUncommittedStatusViaStatus() { + $status = $this->buildLocalFuture( + array( + 'status --porcelain=2 -z', + )); + list($stdout) = $status->resolvex(); + + $result = new PhutilArrayWithDefaultValue(); + $parts = explode("\0", $stdout); + while (count($parts) > 1) { + $entry = array_shift($parts); + $entry_parts = explode(' ', $entry); + if ($entry_parts[0] == '1') { + $path = $entry_parts[8]; + } else if ($entry_parts[0] == '2') { + $path = $entry_parts[9]; + } else if ($entry_parts[0] == 'u') { + $path = $entry_parts[10]; + } else if ($entry_parts[0] == '?') { + $result[$entry_parts[1]] = self::FLAG_UNTRACKED; + continue; + } + + $result[$path] |= self::FLAG_UNCOMMITTED; + $index_state = substr($entry_parts[1], 0, 1); + $working_state = substr($entry_parts[1], 1, 1); + if ($index_state == 'A') { + $result[$path] |= self::FLAG_ADDED; + } else if ($index_state == 'M') { + $result[$path] |= self::FLAG_MODIFIED; + } else if ($index_state == 'D') { + $result[$path] |= self::FLAG_DELETED; + } + if ($working_state != '.') { + $result[$path] |= self::FLAG_UNSTAGED; + if ($index_state == '.') { + if ($working_state == 'A') { + $result[$path] |= self::FLAG_ADDED; + } else if ($working_state == 'M') { + $result[$path] |= self::FLAG_MODIFIED; + } else if ($working_state == 'D') { + $result[$path] |= self::FLAG_DELETED; + } + } + } + $submodule_tracked = substr($entry_parts[2], 2, 1); + $submodule_untracked = substr($entry_parts[2], 3, 1); + if ($submodule_tracked == 'M' || $submodule_untracked == 'U') { + $result[$path] |= self::FLAG_EXTERNALS; + } + + if ($entry_parts[0] == '2') { + $result[array_shift($parts)] = $result[$path] | self::FLAG_DELETED; + $result[$path] |= self::FLAG_ADDED; + } + } + return $result->toArray(); + } protected function buildUncommittedStatus() { + if (version_compare($this->getGitVersion(), '2.11.0', '>=')) { + return $this->buildUncommittedStatusViaStatus(); + } + $diff_options = $this->getDiffBaseOptions(); if ($this->repositoryHasNoCommits) { @@ -719,7 +785,7 @@ protected function buildCommitRangeStatus() { list($stdout, $stderr) = $this->execxLocal( - 'diff %C --raw %s --', + 'diff %C --raw %s HEAD --', $this->getDiffBaseOptions(), $this->getBaseCommit()); diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -372,41 +372,18 @@ } protected function buildCommitRangeStatus() { - // TODO: Possibly we should use "hg status --rev X --rev ." for this - // instead, but we must run "hg diff" later anyway in most cases, so - // building and caching it shouldn't hurt us. + list($stdout) = $this->execxLocal( + 'status --rev %s --rev tip', + $this->getBaseCommit()); - $diff = $this->getFullMercurialDiff(); - if (!$diff) { - return array(); - } + $results = new PhutilArrayWithDefaultValue(); - $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; + $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout); + foreach ($working_status as $path => $mask) { + $results[$path] |= $mask; } - return $status_map; + return $results->toArray(); } protected function didReloadWorkingCopy() { diff --git a/src/repository/api/ArcanistSubversionAPI.php b/src/repository/api/ArcanistSubversionAPI.php --- a/src/repository/api/ArcanistSubversionAPI.php +++ b/src/repository/api/ArcanistSubversionAPI.php @@ -45,9 +45,9 @@ } protected function buildCommitRangeStatus() { - // In SVN, the commit range is always "uncommitted changes", so these - // statuses are equivalent. - return $this->getUncommittedStatus(); + // In SVN, there are never any previous commits in the range -- it is all in + // the uncommitted status. + return array(); } protected function buildUncommittedStatus() { diff --git a/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php --- a/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php +++ b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php @@ -56,6 +56,16 @@ } private function assertCorrectState($test, ArcanistRepositoryAPI $api) { + if ($api instanceof ArcanistGitAPI) { + $version = $api->getGitVersion(); + if (version_compare($version, '2.11.0', '<')) { + // Behavior differs slightly on older versions of git; rather than code + // both variants, skip the tests in the presence of such a git. + $this->assertSkipped(pht('Behavior differs slightly on git < 2.11.0')); + return; + } + } + $f_mod = ArcanistRepositoryAPI::FLAG_MODIFIED; $f_add = ArcanistRepositoryAPI::FLAG_ADDED; $f_del = ArcanistRepositoryAPI::FLAG_DELETED; @@ -70,7 +80,22 @@ switch ($test) { case 'svn_basic.svn.tgz': - $expect = array( + $expect_uncommitted = array( + 'ADDED' => $f_add, + 'COPIED_TO' => $f_add, + 'DELETED' => $f_del, + 'MODIFIED' => $f_mod, + 'MOVED' => $f_del, + 'MOVED_TO' => $f_add, + 'PROPCHANGE' => $f_mod, + 'UNTRACKED' => $f_unt, + ); + $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); + + $expect_range = array(); + $this->assertEqual($expect_range, $api->getCommitRangeStatus()); + + $expect_working = array( 'ADDED' => $f_add, 'COPIED_TO' => $f_add, 'DELETED' => $f_del, @@ -80,8 +105,7 @@ 'PROPCHANGE' => $f_mod, 'UNTRACKED' => $f_unt, ); - $this->assertEqual($expect, $api->getUncommittedStatus()); - $this->assertEqual($expect, $api->getCommitRangeStatus()); + $this->assertEqual($expect_working, $api->getWorkingCopyStatus()); break; case 'git_basic.git.tgz': $expect_uncommitted = array( @@ -96,10 +120,19 @@ 'ADDED' => $f_add, 'DELETED' => $f_del, 'MODIFIED' => $f_mod, - 'UNCOMMITTED' => $f_add, 'UNSTAGED' => $f_add, ); $this->assertEqual($expect_range, $api->getCommitRangeStatus()); + + $expect_working = array( + 'ADDED' => $f_add, + 'DELETED' => $f_del, + 'MODIFIED' => $f_mod, + 'UNCOMMITTED' => $f_add | $f_unc, + 'UNSTAGED' => $f_add | $f_mod | $f_uns | $f_unc, + 'UNTRACKED' => $f_unt, + ); + $this->assertEqual($expect_working, $api->getWorkingCopyStatus()); break; case 'git_submodules_dirty.git.tgz': $expect_uncommitted = array( @@ -107,19 +140,19 @@ 'added/' => $f_unt, 'deleted' => $f_del | $f_uns | $f_unc, 'modified-commit' => $f_mod | $f_uns | $f_unc, - 'modified-commit-dirty' => $f_mod | $f_uns | $f_unc, - 'modified-dirty' => $f_ext | $f_uns | $f_unc, + 'modified-commit-dirty' => $f_ext | $f_mod | $f_uns | $f_unc, + 'modified-dirty' => $f_ext | $f_mod | $f_uns | $f_unc, ); $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); break; case 'git_submodules_staged.git.tgz': $expect_uncommitted = array( - '.gitmodules' => $f_mod | $f_uns | $f_unc, + '.gitmodules' => $f_mod | $f_unc, 'added' => $f_add | $f_unc, 'deleted' => $f_del | $f_unc, 'modified-commit' => $f_mod | $f_unc, - 'modified-commit-dirty' => $f_mod | $f_uns | $f_unc, - 'modified-dirty' => $f_ext | $f_uns | $f_unc, + 'modified-commit-dirty' => $f_ext | $f_mod | $f_uns | $f_unc, + 'modified-dirty' => $f_ext | $f_mod | $f_uns | $f_unc, ); $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); break; @@ -137,6 +170,15 @@ 'UNCOMMITTED' => $f_add, ); $this->assertEqual($expect_range, $api->getCommitRangeStatus()); + + $expect_working = array( + 'ADDED' => $f_add, + 'DELETED' => $f_del, + 'MODIFIED' => $f_mod, + 'UNCOMMITTED' => $f_add | $f_mod | $f_unc, + 'UNTRACKED' => $f_unt, + ); + $this->assertEqual($expect_working, $api->getWorkingCopyStatus()); break; default: throw new Exception(