Page MenuHomePhabricator

D21314.diff
No OneTemporary

D21314.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -219,6 +219,7 @@
'ArcanistGitCommitMessageHardpointQuery' => 'query/ArcanistGitCommitMessageHardpointQuery.php',
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php',
'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php',
+ 'ArcanistGitLocalState' => 'repository/state/ArcanistGitLocalState.php',
'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php',
'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php',
'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'query/ArcanistGitWorkingCopyRevisionHardpointQuery.php',
@@ -401,6 +402,7 @@
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php',
'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php',
+ 'ArcanistRepositoryLocalState' => 'repository/state/ArcanistRepositoryLocalState.php',
'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php',
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php',
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php',
@@ -1227,6 +1229,7 @@
'ArcanistGitCommitMessageHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
'ArcanistGitLandEngine' => 'ArcanistLandEngine',
+ 'ArcanistGitLocalState' => 'ArcanistRepositoryLocalState',
'ArcanistGitUpstreamPath' => 'Phobject',
'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy',
'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
@@ -1412,6 +1415,7 @@
'ArcanistRepositoryAPI' => 'Phobject',
'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase',
'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase',
+ 'ArcanistRepositoryLocalState' => 'Phobject',
'ArcanistRepositoryRef' => 'ArcanistRef',
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
diff --git a/src/repository/state/ArcanistGitLocalState.php b/src/repository/state/ArcanistGitLocalState.php
new file mode 100644
--- /dev/null
+++ b/src/repository/state/ArcanistGitLocalState.php
@@ -0,0 +1,147 @@
+<?php
+
+final class ArcanistGitLocalState
+ extends ArcanistRepositoryLocalState {
+
+ private $localCommit;
+ private $localRef;
+ private $localPath;
+
+ public function getLocalRef() {
+ return $this->localRef;
+ }
+
+ public function getLocalPath() {
+ return $this->localPath;
+ }
+
+ protected function executeSaveLocalState() {
+ $api = $this->getRepositoryAPI();
+
+ $commit = $api->getWorkingCopyRevision();
+
+ list($ref) = $api->execxLocal('rev-parse --abbrev-ref HEAD');
+ $ref = trim($ref);
+ if ($ref === 'HEAD') {
+ $ref = null;
+ $where = pht(
+ 'Saving local state (at detached commit "%s").',
+ $this->getDisplayHash($commit));
+ } else {
+ $where = pht(
+ 'Saving local state (on ref "%s" at commit "%s").',
+ $ref,
+ $this->getDisplayHash($commit));
+ }
+
+ $this->localRef = $ref;
+ $this->localCommit = $commit;
+
+ if ($ref !== null) {
+ $this->localPath = $api->getPathToUpstream($ref);
+ }
+
+ $log = $this->getWorkflow()->getLogEngine();
+ $log->writeStatus(pht('SAVE STATE'), $where);
+ }
+
+ protected function executeRestoreLocalState() {
+ $api = $this->getRepositoryAPI();
+
+ $log = $this->getWorkflow()->getLogEngine();
+
+ $ref = $this->localRef;
+ $commit = $this->localCommit;
+
+ if ($ref !== null) {
+ $where = pht(
+ 'Restoring local state (to ref "%s" at commit "%s").',
+ $ref,
+ $this->getDisplayHash($commit));
+ } else {
+ $where = pht(
+ 'Restoring local state (to detached commit "%s").',
+ $this->getDisplayHash($commit));
+ }
+
+ $log->writeStatus(pht('LOAD STATE'), $where);
+
+ if ($ref !== null) {
+ $api->execxLocal('checkout -B %s %s --', $ref, $commit);
+
+ // TODO: We save, but do not restore, the upstream configuration of
+ // this branch.
+
+ } else {
+ $api->execxLocal('checkout %s --', $commit);
+ }
+
+ $api->execxLocal('submodule update --init --recursive');
+ }
+
+ protected function executeDiscardLocalState() {
+ // We don't have anything to clean up in Git.
+ return;
+ }
+
+ protected function canStashChanges() {
+ return true;
+ }
+
+ protected function getIgnoreHints() {
+ return array(
+ pht(
+ 'To configure Git to ignore certain files in this working copy, '.
+ 'add the file paths to "%s".',
+ '.git/info/exclude'),
+ );
+ }
+
+ protected function saveStash() {
+ $api = $this->getRepositoryAPI();
+
+ // NOTE: We'd prefer to "git stash create" here, because using "push"
+ // and "pop" means we're affecting the stash list as a side effect.
+
+ // However, under Git 2.21.1, "git stash create" exits with no output,
+ // no error, and no effect if the working copy contains only untracked
+ // files. For now, accept mutations to the stash list.
+
+ $api->execxLocal('stash push --include-untracked --');
+
+ $log = $this->getWorkflow()->getLogEngine();
+ $log->writeStatus(
+ pht('SAVE STASH'),
+ pht('Saved uncommitted changes from working copy.'));
+
+ return true;
+ }
+
+ protected function restoreStash($stash_ref) {
+ $api = $this->getRepositoryAPI();
+
+ $log = $this->getWorkflow()->getLogEngine();
+ $log->writeStatus(
+ pht('LOAD STASH'),
+ pht('Restoring uncommitted changes to working copy.'));
+
+ // NOTE: Under Git 2.21.1, "git stash apply" does not accept "--".
+ $api->execxLocal('stash apply');
+ }
+
+ protected function discardStash($stash_ref) {
+ $api = $this->getRepositoryAPI();
+
+ // NOTE: Under Git 2.21.1, "git stash drop" does not accept "--".
+ $api->execxLocal('stash drop');
+ }
+
+ private function getDisplayStashRef($stash_ref) {
+ return substr($stash_ref, 0, 12);
+ }
+
+ private function getDisplayHash($hash) {
+ return substr($hash, 0, 12);
+ }
+
+}
diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php
new file mode 100644
--- /dev/null
+++ b/src/repository/state/ArcanistRepositoryLocalState.php
@@ -0,0 +1,245 @@
+<?php
+
+abstract class ArcanistRepositoryLocalState
+ extends Phobject {
+
+ private $repositoryAPI;
+ private $shouldRestore;
+ private $stashRef;
+ private $workflow;
+
+ final public function setWorkflow(ArcanistWorkflow $workflow) {
+ $this->workflow = $workflow;
+ return $this;
+ }
+
+ final public function getWorkflow() {
+ return $this->workflow;
+ }
+
+ final public function setRepositoryAPI(ArcanistRepositoryAPI $api) {
+ $this->repositoryAPI = $api;
+ return $this;
+ }
+
+ final public function getRepositoryAPI() {
+ return $this->repositoryAPI;
+ }
+
+ final public function saveLocalState() {
+ $api = $this->getRepositoryAPI();
+
+ $working_copy_display = tsprintf(
+ " %s: %s\n",
+ pht('Working Copy'),
+ $api->getPath());
+
+ $conflicts = $api->getMergeConflicts();
+ if ($conflicts) {
+ echo tsprintf(
+ "\n%!\n%W\n\n%s\n",
+ pht('MERGE CONFLICTS'),
+ pht('You have merge conflicts in this working copy.'),
+ $working_copy_display);
+
+ $lists = array();
+
+ $lists[] = $this->newDisplayFileList(
+ pht('Merge conflicts in working copy:'),
+ $conflicts);
+
+ $this->printFileLists($lists);
+
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Resolve merge conflicts before proceeding.'));
+ }
+
+ $externals = $api->getDirtyExternalChanges();
+ if ($externals) {
+ $message = pht(
+ '%s submodule(s) have uncommitted or untracked changes:',
+ new PhutilNumber(count($externals)));
+
+ $prompt = pht(
+ 'Ignore the changes to these %s submodule(s) and continue?',
+ new PhutilNumber(count($externals)));
+
+ $list = id(new PhutilConsoleList())
+ ->setWrap(false)
+ ->addItems($externals);
+
+ id(new PhutilConsoleBlock())
+ ->addParagraph($message)
+ ->addList($list)
+ ->draw();
+
+ $ok = phutil_console_confirm($prompt, $default_no = false);
+ if (!$ok) {
+ throw new ArcanistUserAbortException();
+ }
+ }
+
+ $uncommitted = $api->getUncommittedChanges();
+ $unstaged = $api->getUnstagedChanges();
+ $untracked = $api->getUntrackedChanges();
+
+ // We already dealt with externals.
+ $unstaged = array_diff($unstaged, $externals);
+
+ // We only want files which are purely uncommitted.
+ $uncommitted = array_diff($uncommitted, $unstaged);
+ $uncommitted = array_diff($uncommitted, $externals);
+
+ if ($untracked || $unstaged || $uncommitted) {
+ echo tsprintf(
+ "\n%!\n%W\n\n%s\n",
+ pht('UNCOMMITTED CHANGES'),
+ pht('You have uncommitted changes in this working copy.'),
+ $working_copy_display);
+
+ $lists = array();
+
+ $lists[] = $this->newDisplayFileList(
+ pht('Untracked changes in working copy:'),
+ $untracked);
+
+ $lists[] = $this->newDisplayFileList(
+ pht('Unstaged changes in working copy:'),
+ $unstaged);
+
+ $lists[] = $this->newDisplayFileList(
+ pht('Uncommitted changes in working copy:'),
+ $uncommitted);
+
+ $this->printFileLists($lists);
+
+ if ($untracked) {
+ $hints = $this->getIgnoreHints();
+ foreach ($hints as $hint) {
+ echo tsprintf("%?\n", $hint);
+ }
+ }
+
+ if ($this->canStashChanges()) {
+
+ $query = pht('Stash these changes and continue?');
+
+ $this->getWorkflow()
+ ->getPrompt('arc.state.stash')
+ ->setQuery($query)
+ ->execute();
+
+ $stash_ref = $this->saveStash();
+
+ if ($stash_ref === null) {
+ throw new Exception(
+ pht(
+ 'Expected a non-null return from call to "%s->saveStash()".',
+ get_class($this)));
+ }
+
+ $this->stashRef = $stash_ref;
+ } else {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'You can not continue with uncommitted changes. Commit or '.
+ 'discard them before proceeding.'));
+ }
+ }
+
+ $this->executeSaveLocalState();
+ $this->shouldRestore = true;
+
+ return $this;
+ }
+
+ final public function restoreLocalState() {
+ $this->shouldRestore = false;
+
+ $this->executeRestoreLocalState();
+ if ($this->stashRef !== null) {
+ $this->restoreStash($this->stashRef);
+ }
+
+ return $this;
+ }
+
+ final public function discardLocalState() {
+ $this->shouldRestore = false;
+
+ $this->executeDiscardLocalState();
+ if ($this->stashRef !== null) {
+ $this->restoreStash($this->stashRef);
+ $this->discardStash($this->stashRef);
+ $this->stashRef = null;
+ }
+
+ return $this;
+ }
+
+ final public function __destruct() {
+ if ($this->shouldRestore) {
+ $this->restoreLocalState();
+ }
+
+ $this->discardLocalState();
+ }
+
+ protected function canStashChanges() {
+ return false;
+ }
+
+ protected function saveStash() {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ protected function restoreStash($ref) {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ protected function discardStash($ref) {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ abstract protected function executeSaveLocalState();
+ abstract protected function executeRestoreLocalState();
+ abstract protected function executeDiscardLocalState();
+
+ protected function getIgnoreHints() {
+ return array();
+ }
+
+ final protected function newDisplayFileList($title, array $files) {
+ if (!$files) {
+ return null;
+ }
+
+ $items = array();
+ $items[] = tsprintf("%s\n\n", $title);
+ foreach ($files as $file) {
+ $items[] = tsprintf(
+ " %s\n",
+ $file);
+ }
+
+ return $items;
+ }
+
+ final protected function printFileLists(array $lists) {
+ $lists = array_filter($lists);
+
+ $last_key = last_key($lists);
+ foreach ($lists as $key => $list) {
+ foreach ($list as $item) {
+ echo tsprintf('%B', $item);
+ }
+ if ($key !== $last_key) {
+ echo tsprintf("\n\n");
+ }
+ }
+
+ echo tsprintf("\n");
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Sat, Mar 8, 12:55 PM (4 d, 5 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7382415
Default Alt Text
D21314.diff (12 KB)

Event Timeline