1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-21 22:32:41 +01:00

Introduce "RepositoryLocalState", a modern version of "requireCleanWorkingCopy()"

Summary:
Ref T13546. Introduces a more structured construct for saving and restoring local repository state.

This is similar to old behavior, except that:

  - "arc.autostash" is no longer respected;
  - untracked changes are stashed; and
  - we do not offer to amend.

Test Plan: In future changes, saved and restored various permutations of local state.

Maniphest Tasks: T13546

Differential Revision: https://secure.phabricator.com/D21314
This commit is contained in:
epriestley 2020-06-04 17:27:29 -07:00
parent 7ac3b791b0
commit 0da395ffe4
3 changed files with 396 additions and 0 deletions

View file

@ -219,6 +219,7 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'ArcanistGitCommitMessageHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
'ArcanistGitLandEngine' => 'ArcanistLandEngine',
'ArcanistGitLocalState' => 'ArcanistRepositoryLocalState',
'ArcanistGitUpstreamPath' => 'Phobject',
'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy',
'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
@ -1412,6 +1415,7 @@ phutil_register_library_map(array(
'ArcanistRepositoryAPI' => 'Phobject',
'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase',
'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase',
'ArcanistRepositoryLocalState' => 'Phobject',
'ArcanistRepositoryRef' => 'ArcanistRef',
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',

View file

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

View file

@ -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");
}
}