From 0e27dbfd17a4587daf8645170eab6b7a83a28336 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 17 Dec 2012 12:53:28 -0800 Subject: [PATCH] Refactor getWorkingCopyStatus() into getUncommittedStatus() and getCommitRangeStatus() Summary: See discussion in D4049. The getWorkingCopyStatus() method gets called from requireCleanWorkingCopy() in a lot of places, which triggers resolution of the base of the commit range. This is unnecessary; we do not need to examine the base commit in order to determine whether the working copy is dirty or not. This causes problems, in D4049 and elsewhere (we currently have a lot of fluff calls to setDefaultBaseCommit() in workflows which need to call requireCleanWorkingCopy() but do not ever use commit ranges, such as `arc patch`). This is mostly an artifact of SVN, where the "commit range" and "uncommitted stuff in the working copy" are always the same. - Split the method into two status methods: getUncommittedStatus() (uncommitted stuff in the working copy, required by requireCleanWorkingCopy()) and getCommitRangeStatus() (committed stuff in the commit range). - Lift caching out of the implementations into the base class. - Dirty the cache after we commit. This doesn't do anything useful on its own and creates one caching problem (`commitRangeStatusCache` is not invalidated when the commit range changes because of `setBaseCommit()` or similar) but I wanted to break things apart here. I won't land it until there's a more complete picture. This creates a minor performance regression in git and hg (we run less stuff in parallel than previously) but all the commands should be disk-bound anyway and the regression should be minor. It prevents a larger regression in `hg` in D4049, and lets us do less work to arrive at common error states (dirty working copy). We can examine perf at the end of this change sequence. Test Plan: Ran unit tests, various `arc` commands. Reviewers: vrana Reviewed By: vrana CC: aran Differential Revision: https://secure.phabricator.com/D4095 --- src/__phutil_library_map__.php | 2 + src/repository/api/ArcanistGitAPI.php | 162 ++++++++-------- src/repository/api/ArcanistMercurialAPI.php | 114 ++++++------ src/repository/api/ArcanistRepositoryAPI.php | 175 ++++++++++++++++-- src/repository/api/ArcanistSubversionAPI.php | 8 +- .../ArcanistRepositoryAPIStateTestCase.php | 93 ++++++++++ .../api/__tests__/state/git_basic.git.tgz | Bin 0 -> 8515 bytes .../api/__tests__/state/hg_basic.hg.tgz | Bin 0 -> 1645 bytes .../api/__tests__/state/svn_basic.svn.tgz | Bin 0 -> 5096 bytes 9 files changed, 394 insertions(+), 160 deletions(-) create mode 100644 src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php create mode 100644 src/repository/api/__tests__/state/git_basic.git.tgz create mode 100644 src/repository/api/__tests__/state/hg_basic.hg.tgz create mode 100644 src/repository/api/__tests__/state/svn_basic.svn.tgz diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 33e38b8d..6eebb90d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -103,6 +103,7 @@ phutil_register_library_map(array( 'ArcanistPyFlakesLinter' => 'lint/linter/ArcanistPyFlakesLinter.php', 'ArcanistPyLintLinter' => 'lint/linter/ArcanistPyLintLinter.php', 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', + 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', 'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php', 'ArcanistRubyLinterTestCase' => 'lint/linter/__tests__/ArcanistRubyLinterTestCase.php', 'ArcanistScalaSBTLinter' => 'lint/linter/ArcanistScalaSBTLinter.php', @@ -216,6 +217,7 @@ phutil_register_library_map(array( 'ArcanistPhutilTestTerminatedException' => 'Exception', 'ArcanistPyFlakesLinter' => 'ArcanistLinter', 'ArcanistPyLintLinter' => 'ArcanistLinter', + 'ArcanistRepositoryAPIStateTestCase' => 'ArcanistTestCase', 'ArcanistRubyLinter' => 'ArcanistLinter', 'ArcanistRubyLinterTestCase' => 'ArcanistArcanistLinterTestCase', 'ArcanistScalaSBTLinter' => 'ArcanistLinter', diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index 1cfd5d26..f3de6260 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -7,14 +7,13 @@ */ final class ArcanistGitAPI extends ArcanistRepositoryAPI { - private $status; private $relativeCommit = null; private $repositoryHasNoCommits = false; const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16; /** * For the repository's initial commit, 'git diff HEAD^' and similar do - * not work. Using this instead does work. + * not work. Using this instead does work; it is the hash of the empty tree. */ const GIT_MAGIC_ROOT_COMMIT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; @@ -384,92 +383,81 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return rtrim($stdout); } - public function getWorkingCopyStatus() { - if (!isset($this->status)) { + protected function buildUncommittedStatus() { + $diff_options = $this->getDiffBaseOptions(); - $options = $this->getDiffBaseOptions(); - - // -- parallelize these slow cpu bound git calls. - - // Find committed changes. - $committed_future = $this->buildLocalFuture( - array( - "diff {$options} --raw %s --", - $this->getRelativeCommit(), - )); - - // Find uncommitted changes. - $uncommitted_future = $this->buildLocalFuture( - array( - "diff {$options} --raw %s --", - $this->repositoryHasNoCommits - ? self::GIT_MAGIC_ROOT_COMMIT - : 'HEAD', - )); - - // Untracked files - $untracked_future = $this->buildLocalFuture( - array( - 'ls-files --others --exclude-standard', - )); - - // TODO: This doesn't list unstaged adds. It's not clear how to get that - // list other than "git status --porcelain" and then parsing it. :/ - - // Unstaged changes - $unstaged_future = $this->buildLocalFuture( - array( - 'ls-files -m', - )); - - $futures = array( - $committed_future, - $uncommitted_future, - $untracked_future, - $unstaged_future - ); - Futures($futures)->resolveAll(); - - - // -- read back and process the results - - list($stdout, $stderr) = $committed_future->resolvex(); - $files = $this->parseGitStatus($stdout); - - list($stdout, $stderr) = $uncommitted_future->resolvex(); - $uncommitted_files = $this->parseGitStatus($stdout); - foreach ($uncommitted_files as $path => $mask) { - $mask |= self::FLAG_UNCOMMITTED; - if (!isset($files[$path])) { - $files[$path] = 0; - } - $files[$path] |= $mask; - } - - list($stdout, $stderr) = $untracked_future->resolvex(); - $stdout = rtrim($stdout, "\n"); - if (strlen($stdout)) { - $stdout = explode("\n", $stdout); - foreach ($stdout as $file) { - $files[$file] = self::FLAG_UNTRACKED; - } - } - - list($stdout, $stderr) = $unstaged_future->resolvex(); - $stdout = rtrim($stdout, "\n"); - if (strlen($stdout)) { - $stdout = explode("\n", $stdout); - foreach ($stdout as $file) { - $files[$file] = isset($files[$file]) - ? ($files[$file] | self::FLAG_UNSTAGED) - : self::FLAG_UNSTAGED; - } - } - - $this->status = $files; + if ($this->repositoryHasNoCommits) { + $diff_base = self::GIT_MAGIC_ROOT_COMMIT; + } else { + $diff_base = 'HEAD'; } - return $this->status; + // Find uncommitted changes. + $uncommitted_future = $this->buildLocalFuture( + array( + 'diff %C --raw %s --', + $diff_options, + $diff_base, + )); + + $untracked_future = $this->buildLocalFuture( + array( + 'ls-files --others --exclude-standard', + )); + + // TODO: This doesn't list unstaged adds. It's not clear how to get that + // list other than "git status --porcelain" and then parsing it. :/ + + // Unstaged changes + $unstaged_future = $this->buildLocalFuture( + array( + 'ls-files -m', + )); + + $futures = array( + $uncommitted_future, + $untracked_future, + $unstaged_future, + ); + + Futures($futures)->resolveAll(); + + $result = new PhutilArrayWithDefaultValue(); + + list($stdout) = $uncommitted_future->resolvex(); + $uncommitted_files = $this->parseGitStatus($stdout); + foreach ($uncommitted_files as $path => $mask) { + $result[$path] |= ($mask | self::FLAG_UNCOMMITTED); + } + + list($stdout) = $untracked_future->resolvex(); + $stdout = rtrim($stdout, "\n"); + if (strlen($stdout)) { + $stdout = explode("\n", $stdout); + foreach ($stdout as $path) { + $result[$path] |= self::FLAG_UNTRACKED; + } + } + + list($stdout, $stderr) = $unstaged_future->resolvex(); + $stdout = rtrim($stdout, "\n"); + if (strlen($stdout)) { + $stdout = explode("\n", $stdout); + foreach ($stdout as $path) { + $result[$path] |= self::FLAG_UNSTAGED; + } + } + + return $result->toArray(); + } + + protected function buildCommitRangeStatus() { + list($stdout, $stderr) = $this->execxLocal( + 'diff %C --raw %s --', + $this->getDiffBaseOptions(), + $this->getRelativeCommit()); + + return $this->parseGitStatus($stdout); } public function getGitConfig($key, $default = null) { @@ -497,6 +485,10 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { $this->execxLocal( 'commit --allow-empty-message -F %s', $tmp_file); + + $this->reloadWorkingCopy(); + + return $this; } public function amendCommit($message) { diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index e79e121d..50e28014 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -7,7 +7,6 @@ */ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { - private $status; private $base; private $relativeCommit; private $branch; @@ -296,70 +295,66 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return $blame; } - public function getWorkingCopyStatus() { + protected function buildUncommittedStatus() { + list($stdout) = $this->execxLocal('status'); - 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. + $results = new PhutilArrayWithDefaultValue(); - // Execute status in the background - $status_future = $this->buildLocalFuture(array('status')); - $status_future->start(); - - $diff = $this->getFullMercurialDiff(); - - if (!$diff) { - $this->status = array(); - return $this->status; + $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; } - $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) = $status_future->resolvex(); - - $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; + $results[$path] |= $mask; } - return $this->status; + return $results->toArray(); + } + + 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. + + $diff = $this->getFullMercurialDiff(); + if (!$diff) { + return array(); + } + + $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; + } + + return $status_map; + } + + protected function didReloadWorkingCopy() { + // Diffs are against ".", so we need to drop the cache if we change the + // working copy. + $this->rawDiffCache = array(); } private function getDiffOptions() { @@ -629,6 +624,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { $this->execxLocal( 'commit -l %s', $tmp_file); + $this->reloadWorkingCopy(); } public function amendCommit($message) { diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 0e4334e5..9af523ab 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -3,6 +3,7 @@ /** * Interfaces with the VCS in the working copy. * + * @task status Path Status * @group workingcopy */ abstract class ArcanistRepositoryAPI { @@ -30,6 +31,9 @@ abstract class ArcanistRepositoryAPI { private $workingCopyIdentity; private $baseCommitArgumentRules; + private $uncommittedStatusCache; + private $commitRangeStatusCache; + abstract public function getSourceControlSystemName(); public function getDiffLinesOfContext() { @@ -76,7 +80,9 @@ abstract class ArcanistRepositoryAPI { } // check if we're in an svn working copy - list($err) = exec_manual('svn info'); + list($err) = id(new ExecFuture('svn info')) + ->setCWD($root) + ->resolve(); if (!$err) { $api = new ArcanistSubversionAPI($root); $api->workingCopyIdentity = $working_copy; @@ -101,36 +107,174 @@ abstract class ArcanistRepositoryAPI { } } - public function getUntrackedChanges() { - return $this->getWorkingCopyFilesWithMask(self::FLAG_UNTRACKED); + +/* -( Path Status )-------------------------------------------------------- */ + + + abstract protected function buildUncommittedStatus(); + abstract protected function buildCommitRangeStatus(); + + + /** + * Get a list of uncommitted paths in the working copy that have been changed + * or are affected by other status effects, like conflicts or untracked + * files. + * + * Convenience methods @{method:getUntrackedChanges}, + * @{method:getUnstagedChanges}, @{method:getUncommittedChanges}, + * @{method:getMergeConflicts}, and @{method:getIncompleteChanges} allow + * simpler selection of paths in a specific state. + * + * This method returns a map of paths to bitmasks with status, using + * `FLAG_` constants. For example: + * + * array( + * 'some/uncommitted/file.txt' => ArcanistRepositoryAPI::FLAG_UNSTAGED, + * ); + * + * A file may be in several states. Not all states are possible with all + * version control systems. + * + * @return map Map of paths, see above. + * @task status + */ + final public function getUncommittedStatus() { + if ($this->uncommittedStatusCache === null) { + $status = $this->buildUncommittedStatus();; + ksort($status); + $this->uncommittedStatusCache = $status; + } + return $this->uncommittedStatusCache; } - public function getUnstagedChanges() { - return $this->getWorkingCopyFilesWithMask(self::FLAG_UNSTAGED); + + /** + * @task status + */ + final public function getUntrackedChanges() { + return $this->getUncommittedPathsWithMask(self::FLAG_UNTRACKED); } - public function getUncommittedChanges() { - return $this->getWorkingCopyFilesWithMask(self::FLAG_UNCOMMITTED); + + /** + * @task status + */ + final public function getUnstagedChanges() { + return $this->getUncommittedPathsWithMask(self::FLAG_UNSTAGED); } - public function getMergeConflicts() { - return $this->getWorkingCopyFilesWithMask(self::FLAG_CONFLICT); + + /** + * @task status + */ + final public function getUncommittedChanges() { + return $this->getUncommittedPathsWithMask(self::FLAG_UNCOMMITTED); } - public function getIncompleteChanges() { - return $this->getWorkingCopyFilesWithMask(self::FLAG_INCOMPLETE); + + /** + * @task status + */ + final public function getMergeConflicts() { + return $this->getUncommittedPathsWithMask(self::FLAG_CONFLICT); } - private function getWorkingCopyFilesWithMask($mask) { + + /** + * @task status + */ + final public function getIncompleteChanges() { + return $this->getUncommittedPathsWithMask(self::FLAG_INCOMPLETE); + } + + + /** + * @task status + */ + private function getUncommittedPathsWithMask($mask) { $match = array(); - foreach ($this->getWorkingCopyStatus() as $file => $flags) { + foreach ($this->getUncommittedStatus() as $path => $flags) { if ($flags & $mask) { - $match[] = $file; + $match[] = $path; } } return $match; } + + /** + * Get a list of paths affected by the commits in the current commit range. + * + * See @{method:getUncommittedStatus} for a description of the return value. + * + * @return map Map from paths to status. + * @task status + */ + final public function getCommitRangeStatus() { + if ($this->commitRangeStatusCache === null) { + $status = $this->buildCommitRangeStatus(); + ksort($status); + $this->commitRangeStatusCache = $status; + } + return $this->commitRangeStatusCache; + } + + + /** + * Get a list of paths affected by commits in the current commit range, or + * uncommitted changes in the working copy. See @{method:getUncommittedStatus} + * or @{method:getCommitRangeStatus} to retreive smaller parts of the status. + * + * See @{method:getUncommittedStatus} for a description of the return value. + * + * @return map Map from paths to status. + * @task status + */ + final public function getWorkingCopyStatus() { + $range_status = $this->getCommitRangeStatus(); + $uncommitted_status = $this->getUncommittedStatus(); + + $result = new PhutilArrayWithDefaultValue($range_status); + foreach ($uncommitted_status as $path => $mask) { + $result[$path] |= $mask; + } + + $result = $result->toArray(); + ksort($result); + return $result; + } + + + /** + * Drops caches after changes to the working copy. By default, some queries + * against the working copy are cached. They + * + * @return this + * @task status + */ + final public function reloadWorkingCopy() { + $this->uncommittedStatusCache = null; + $this->commitRangeStatusCache = null; + + $this->didReloadWorkingCopy(); + + return $this; + } + + + /** + * Hook for implementations to dirty working copy caches after the working + * copy has been updated. + * + * @return this + * @task status + */ + protected function didReloadWorkingCopy() { + return; + } + + + private static function discoverGitBaseDirectory($root) { try { @@ -148,13 +292,14 @@ abstract class ArcanistRepositoryAPI { } } + /** * @return Traversable */ abstract public function getAllFiles(); abstract public function getBlame($path); - abstract public function getWorkingCopyStatus(); + abstract public function getRawDiffText($path); abstract public function getOriginalFileData($path); abstract public function getCurrentFileData($path); diff --git a/src/repository/api/ArcanistSubversionAPI.php b/src/repository/api/ArcanistSubversionAPI.php index 82c9402e..7fae4703 100644 --- a/src/repository/api/ArcanistSubversionAPI.php +++ b/src/repository/api/ArcanistSubversionAPI.php @@ -46,7 +46,13 @@ final class ArcanistSubversionAPI extends ArcanistRepositoryAPI { return $future; } - public function getWorkingCopyStatus() { + protected function buildCommitRangeStatus() { + // In SVN, the commit range is always "uncommitted changes", so these + // statuses are equivalent. + return $this->getUncommittedStatus(); + } + + protected function buildUncommittedStatus() { return $this->getSVNStatus(); } diff --git a/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php new file mode 100644 index 00000000..e139d3d0 --- /dev/null +++ b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php @@ -0,0 +1,93 @@ +getPath(); + $working_copy = ArcanistWorkingCopyIdentity::newFromPath($fixture_path); + + $api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity( + $working_copy); + + if ($api->supportsRelativeLocalCommits()) { + $api->setDefaultBaseCommit(); + } + + $this->assertCorrectState($test, $api); + } + } + + private function assertCorrectState($test, ArcanistRepositoryAPI $api) { + $f_mod = ArcanistRepositoryAPI::FLAG_MODIFIED; + $f_add = ArcanistRepositoryAPI::FLAG_ADDED; + $f_del = ArcanistRepositoryAPI::FLAG_DELETED; + $f_unt = ArcanistRepositoryAPI::FLAG_UNTRACKED; + $f_con = ArcanistRepositoryAPI::FLAG_CONFLICT; + $f_mis = ArcanistRepositoryAPI::FLAG_MISSING; + $f_uns = ArcanistRepositoryAPI::FLAG_UNSTAGED; + $f_unc = ArcanistRepositoryAPI::FLAG_UNCOMMITTED; + $f_ext = ArcanistRepositoryAPI::FLAG_EXTERNALS; + $f_obs = ArcanistRepositoryAPI::FLAG_OBSTRUCTED; + $f_inc = ArcanistRepositoryAPI::FLAG_INCOMPLETE; + + switch ($test) { + case 'svn_basic.svn.tgz': + $expect = 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, $api->getUncommittedStatus()); + $this->assertEqual($expect, $api->getCommitRangeStatus()); + break; + case 'git_basic.git.tgz': + $expect_uncommitted = array( + 'UNCOMMITTED' => $f_add | $f_unc, + 'UNSTAGED' => $f_mod | $f_uns | $f_unc, + 'UNTRACKED' => $f_unt, + ); + $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); + + $expect_range = array( + 'ADDED' => $f_add, + 'DELETED' => $f_del, + 'MODIFIED' => $f_mod, + 'UNCOMMITTED' => $f_add, + 'UNSTAGED' => $f_add, + ); + $this->assertEqual($expect_range, $api->getCommitRangeStatus()); + break; + case 'hg_basic.hg.tgz': + $expect_uncommitted = array( + 'UNCOMMITTED' => $f_mod | $f_unc, + 'UNTRACKED' => $f_unt, + ); + $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); + + $expect_range = array( + 'ADDED' => $f_add, + 'DELETED' => $f_del, + 'MODIFIED' => $f_mod, + 'UNCOMMITTED' => $f_add, + ); + $this->assertEqual($expect_range, $api->getCommitRangeStatus()); + break; + default: + throw new Exception( + "No test cases for working copy '{$test}'!"); + } + } + + +} diff --git a/src/repository/api/__tests__/state/git_basic.git.tgz b/src/repository/api/__tests__/state/git_basic.git.tgz new file mode 100644 index 0000000000000000000000000000000000000000..d1841a8f7d2fade1fc33ca44442547bbc9177c2a GIT binary patch literal 8515 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 g+~QYj=6