mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-25 00:02:40 +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:
parent
7ac3b791b0
commit
0da395ffe4
3 changed files with 396 additions and 0 deletions
|
@ -219,6 +219,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistGitCommitMessageHardpointQuery' => 'query/ArcanistGitCommitMessageHardpointQuery.php',
|
'ArcanistGitCommitMessageHardpointQuery' => 'query/ArcanistGitCommitMessageHardpointQuery.php',
|
||||||
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php',
|
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php',
|
||||||
'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php',
|
'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php',
|
||||||
|
'ArcanistGitLocalState' => 'repository/state/ArcanistGitLocalState.php',
|
||||||
'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php',
|
'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php',
|
||||||
'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php',
|
'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php',
|
||||||
'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'query/ArcanistGitWorkingCopyRevisionHardpointQuery.php',
|
'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'query/ArcanistGitWorkingCopyRevisionHardpointQuery.php',
|
||||||
|
@ -401,6 +402,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
|
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
|
||||||
'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php',
|
'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php',
|
||||||
'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php',
|
'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php',
|
||||||
|
'ArcanistRepositoryLocalState' => 'repository/state/ArcanistRepositoryLocalState.php',
|
||||||
'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php',
|
'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php',
|
||||||
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php',
|
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php',
|
||||||
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php',
|
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php',
|
||||||
|
@ -1227,6 +1229,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistGitCommitMessageHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
|
'ArcanistGitCommitMessageHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
|
||||||
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
|
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
|
||||||
'ArcanistGitLandEngine' => 'ArcanistLandEngine',
|
'ArcanistGitLandEngine' => 'ArcanistLandEngine',
|
||||||
|
'ArcanistGitLocalState' => 'ArcanistRepositoryLocalState',
|
||||||
'ArcanistGitUpstreamPath' => 'Phobject',
|
'ArcanistGitUpstreamPath' => 'Phobject',
|
||||||
'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy',
|
'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy',
|
||||||
'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
|
'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
|
||||||
|
@ -1412,6 +1415,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistRepositoryAPI' => 'Phobject',
|
'ArcanistRepositoryAPI' => 'Phobject',
|
||||||
'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase',
|
'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase',
|
||||||
'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase',
|
'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase',
|
||||||
|
'ArcanistRepositoryLocalState' => 'Phobject',
|
||||||
'ArcanistRepositoryRef' => 'ArcanistRef',
|
'ArcanistRepositoryRef' => 'ArcanistRef',
|
||||||
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
|
|
147
src/repository/state/ArcanistGitLocalState.php
Normal file
147
src/repository/state/ArcanistGitLocalState.php
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
245
src/repository/state/ArcanistRepositoryLocalState.php
Normal file
245
src/repository/state/ArcanistRepositoryLocalState.php
Normal 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue