mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-21 22:32:41 +01:00
Substantially modernize the "arc land" workflow
Summary: Ref T13546. This has a lot of dangerously rough edges, but has managed to land at least one commit in each Git and Mercurial. Test Plan: - Landed one commit under ideal conditions in Git and Mercurial. - See followups. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21315
This commit is contained in:
parent
7d615a97e2
commit
68f28a1718
22 changed files with 4226 additions and 2686 deletions
|
@ -218,7 +218,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php',
|
||||
'ArcanistGitCommitMessageHardpointQuery' => 'query/ArcanistGitCommitMessageHardpointQuery.php',
|
||||
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php',
|
||||
'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php',
|
||||
'ArcanistGitLandEngine' => 'land/engine/ArcanistGitLandEngine.php',
|
||||
'ArcanistGitLocalState' => 'repository/state/ArcanistGitLocalState.php',
|
||||
'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php',
|
||||
'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php',
|
||||
|
@ -288,7 +288,11 @@ phutil_register_library_map(array(
|
|||
'ArcanistKeywordCasingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistKeywordCasingXHPASTLinterRuleTestCase.php',
|
||||
'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLambdaFuncFunctionXHPASTLinterRule.php',
|
||||
'ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase.php',
|
||||
'ArcanistLandEngine' => 'land/ArcanistLandEngine.php',
|
||||
'ArcanistLandCommit' => 'land/ArcanistLandCommit.php',
|
||||
'ArcanistLandCommitSet' => 'land/ArcanistLandCommitSet.php',
|
||||
'ArcanistLandEngine' => 'land/engine/ArcanistLandEngine.php',
|
||||
'ArcanistLandSymbol' => 'land/ArcanistLandSymbol.php',
|
||||
'ArcanistLandTarget' => 'land/ArcanistLandTarget.php',
|
||||
'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php',
|
||||
'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLanguageConstructParenthesesXHPASTLinterRule.php',
|
||||
'ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase.php',
|
||||
|
@ -320,9 +324,13 @@ phutil_register_library_map(array(
|
|||
'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php',
|
||||
'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase.php',
|
||||
'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php',
|
||||
'ArcanistMercurialCommitMessageHardpointQuery' => 'query/ArcanistMercurialCommitMessageHardpointQuery.php',
|
||||
'ArcanistMercurialLandEngine' => 'land/engine/ArcanistMercurialLandEngine.php',
|
||||
'ArcanistMercurialLocalState' => 'repository/state/ArcanistMercurialLocalState.php',
|
||||
'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php',
|
||||
'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php',
|
||||
'ArcanistMercurialWorkingCopy' => 'workingcopy/ArcanistMercurialWorkingCopy.php',
|
||||
'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php',
|
||||
'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php',
|
||||
'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php',
|
||||
'ArcanistMessageRevisionHardpointQuery' => 'query/ArcanistMessageRevisionHardpointQuery.php',
|
||||
|
@ -524,6 +532,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistWorkflowArgument' => 'toolset/ArcanistWorkflowArgument.php',
|
||||
'ArcanistWorkflowGitHardpointQuery' => 'query/ArcanistWorkflowGitHardpointQuery.php',
|
||||
'ArcanistWorkflowInformation' => 'toolset/ArcanistWorkflowInformation.php',
|
||||
'ArcanistWorkflowMercurialHardpointQuery' => 'query/ArcanistWorkflowMercurialHardpointQuery.php',
|
||||
'ArcanistWorkingCopy' => 'workingcopy/ArcanistWorkingCopy.php',
|
||||
'ArcanistWorkingCopyCommitHardpointQuery' => 'query/ArcanistWorkingCopyCommitHardpointQuery.php',
|
||||
'ArcanistWorkingCopyConfigurationSource' => 'config/source/ArcanistWorkingCopyConfigurationSource.php',
|
||||
|
@ -1298,8 +1307,12 @@ phutil_register_library_map(array(
|
|||
'ArcanistKeywordCasingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||
'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||
'ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||
'ArcanistLandCommit' => 'Phobject',
|
||||
'ArcanistLandCommitSet' => 'Phobject',
|
||||
'ArcanistLandEngine' => 'Phobject',
|
||||
'ArcanistLandWorkflow' => 'ArcanistWorkflow',
|
||||
'ArcanistLandSymbol' => 'Phobject',
|
||||
'ArcanistLandTarget' => 'Phobject',
|
||||
'ArcanistLandWorkflow' => 'ArcanistArcWorkflow',
|
||||
'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||
'ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||
'ArcanistLesscLinter' => 'ArcanistExternalLinter',
|
||||
|
@ -1330,9 +1343,13 @@ phutil_register_library_map(array(
|
|||
'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||
'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
|
||||
'ArcanistMercurialCommitMessageHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery',
|
||||
'ArcanistMercurialLandEngine' => 'ArcanistLandEngine',
|
||||
'ArcanistMercurialLocalState' => 'ArcanistRepositoryLocalState',
|
||||
'ArcanistMercurialParser' => 'Phobject',
|
||||
'ArcanistMercurialParserTestCase' => 'PhutilTestCase',
|
||||
'ArcanistMercurialWorkingCopy' => 'ArcanistWorkingCopy',
|
||||
'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery',
|
||||
'ArcanistMergeConflictLinter' => 'ArcanistLinter',
|
||||
'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase',
|
||||
'ArcanistMessageRevisionHardpointQuery' => 'ArcanistRuntimeHardpointQuery',
|
||||
|
@ -1544,6 +1561,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistWorkflowArgument' => 'Phobject',
|
||||
'ArcanistWorkflowGitHardpointQuery' => 'ArcanistRuntimeHardpointQuery',
|
||||
'ArcanistWorkflowInformation' => 'Phobject',
|
||||
'ArcanistWorkflowMercurialHardpointQuery' => 'ArcanistRuntimeHardpointQuery',
|
||||
'ArcanistWorkingCopy' => 'Phobject',
|
||||
'ArcanistWorkingCopyCommitHardpointQuery' => 'ArcanistRuntimeHardpointQuery',
|
||||
'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource',
|
||||
|
|
|
@ -136,19 +136,15 @@ final class ArcanistArcConfigurationEngineExtension
|
|||
array(
|
||||
'origin',
|
||||
)),
|
||||
id(new ArcanistBoolConfigOption())
|
||||
->setKey('history.immutable')
|
||||
id(new ArcanistStringConfigOption())
|
||||
->setKey('arc.land.strategy')
|
||||
->setSummary(
|
||||
pht(
|
||||
'Configure use of history mutation operations like amends '.
|
||||
'and rebases.'))
|
||||
'Configure a default merge strategy for "arc land".'))
|
||||
->setHelp(
|
||||
pht(
|
||||
'If this option is set to "true", Arcanist will treat the '.
|
||||
'repository history as immutable and will never issue '.
|
||||
'commands which rewrite repository history (like amends or '.
|
||||
'rebases). This option defaults to "true" in Mercurial, '.
|
||||
'"false" in Git, and has no effect in Subversion.')),
|
||||
'Specifies the default behavior when "arc land" is run with '.
|
||||
'no "--strategy" flag.')),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,913 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistGitLandEngine
|
||||
extends ArcanistLandEngine {
|
||||
|
||||
private $localRef;
|
||||
private $localCommit;
|
||||
private $sourceCommit;
|
||||
private $mergedRef;
|
||||
private $restoreWhenDestroyed;
|
||||
private $isGitPerforce;
|
||||
|
||||
private function setIsGitPerforce($is_git_perforce) {
|
||||
$this->isGitPerforce = $is_git_perforce;
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function getIsGitPerforce() {
|
||||
return $this->isGitPerforce;
|
||||
}
|
||||
|
||||
public function parseArguments() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
$onto = $this->getEngineOnto();
|
||||
$this->setTargetOnto($onto);
|
||||
|
||||
$remote = $this->getEngineRemote();
|
||||
|
||||
$is_pushable = $api->isPushableRemote($remote);
|
||||
$is_perforce = $api->isPerforceRemote($remote);
|
||||
|
||||
if (!$is_pushable && !$is_perforce) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'No pushable remote "%s" exists. Use the "--remote" flag to choose '.
|
||||
'a valid, pushable remote to land changes onto.',
|
||||
$remote));
|
||||
}
|
||||
|
||||
if ($is_perforce) {
|
||||
$this->setIsGitPerforce(true);
|
||||
$this->writeWarn(
|
||||
pht('P4 MODE'),
|
||||
pht(
|
||||
'Operating in Git/Perforce mode after selecting a Perforce '.
|
||||
'remote.'));
|
||||
|
||||
if (!$this->getShouldSquash()) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Perforce mode does not support the "merge" land strategy. '.
|
||||
'Use the "squash" land strategy when landing to a Perforce '.
|
||||
'remote (you can use "--squash" to select this strategy).'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->setTargetRemote($remote);
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
$this->verifySourceAndTargetExist();
|
||||
$this->fetchTarget();
|
||||
|
||||
$this->printLandingCommits();
|
||||
|
||||
if ($this->getShouldPreview()) {
|
||||
$this->writeInfo(
|
||||
pht('PREVIEW'),
|
||||
pht('Completed preview of operation.'));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->saveLocalState();
|
||||
|
||||
try {
|
||||
$this->identifyRevision();
|
||||
$this->updateWorkingCopy();
|
||||
|
||||
if ($this->getShouldHold()) {
|
||||
$this->didHoldChanges();
|
||||
} else {
|
||||
$this->pushChange();
|
||||
$this->reconcileLocalState();
|
||||
|
||||
$api = $this->getRepositoryAPI();
|
||||
$api->execxLocal('submodule update --init --recursive');
|
||||
|
||||
if ($this->getShouldKeep()) {
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht('Keeping local branch.'));
|
||||
} else {
|
||||
$this->destroyLocalBranch();
|
||||
}
|
||||
|
||||
$this->writeOkay(
|
||||
pht('DONE'),
|
||||
pht('Landed changes.'));
|
||||
}
|
||||
|
||||
$this->restoreWhenDestroyed = false;
|
||||
} catch (Exception $ex) {
|
||||
$this->restoreLocalState();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if ($this->restoreWhenDestroyed) {
|
||||
$this->writeWarn(
|
||||
pht('INTERRUPTED!'),
|
||||
pht('Restoring working copy to its original state.'));
|
||||
|
||||
$this->restoreLocalState();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getLandingCommits() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
list($out) = $api->execxLocal(
|
||||
'log --oneline %s..%s --',
|
||||
$this->getTargetFullRef(),
|
||||
$this->sourceCommit);
|
||||
|
||||
$out = trim($out);
|
||||
|
||||
if (!strlen($out)) {
|
||||
return array();
|
||||
} else {
|
||||
return phutil_split_lines($out, false);
|
||||
}
|
||||
}
|
||||
|
||||
private function identifyRevision() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
$api->execxLocal('checkout %s --', $this->getSourceRef());
|
||||
call_user_func($this->getBuildMessageCallback(), $this);
|
||||
}
|
||||
|
||||
private function verifySourceAndTargetExist() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
list($err) = $api->execManualLocal(
|
||||
'rev-parse --verify %s',
|
||||
$this->getTargetFullRef());
|
||||
|
||||
if ($err) {
|
||||
$this->writeWarn(
|
||||
pht('TARGET'),
|
||||
pht(
|
||||
'No local ref exists for branch "%s" in remote "%s", attempting '.
|
||||
'fetch...',
|
||||
$this->getTargetOnto(),
|
||||
$this->getTargetRemote()));
|
||||
|
||||
$api->execManualLocal(
|
||||
'fetch %s %s --',
|
||||
$this->getTargetRemote(),
|
||||
$this->getTargetOnto());
|
||||
|
||||
list($err) = $api->execManualLocal(
|
||||
'rev-parse --verify %s',
|
||||
$this->getTargetFullRef());
|
||||
if ($err) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Branch "%s" does not exist in remote "%s".',
|
||||
$this->getTargetOnto(),
|
||||
$this->getTargetRemote()));
|
||||
}
|
||||
|
||||
$this->writeInfo(
|
||||
pht('FETCHED'),
|
||||
pht(
|
||||
'Fetched branch "%s" from remote "%s".',
|
||||
$this->getTargetOnto(),
|
||||
$this->getTargetRemote()));
|
||||
}
|
||||
|
||||
list($err, $stdout) = $api->execManualLocal(
|
||||
'rev-parse --verify %s',
|
||||
$this->getSourceRef());
|
||||
|
||||
if ($err) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Branch "%s" does not exist in the local working copy.',
|
||||
$this->getSourceRef()));
|
||||
}
|
||||
|
||||
$this->sourceCommit = trim($stdout);
|
||||
}
|
||||
|
||||
private function fetchTarget() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
$ref = $this->getTargetFullRef();
|
||||
|
||||
// NOTE: Although this output isn't hugely useful, we need to passthru
|
||||
// instead of using a subprocess here because `git fetch` may prompt the
|
||||
// user to enter a password if they're fetching over HTTP with basic
|
||||
// authentication. See T10314.
|
||||
|
||||
if ($this->getIsGitPerforce()) {
|
||||
$this->writeInfo(
|
||||
pht('P4 SYNC'),
|
||||
pht('Synchronizing "%s" from Perforce...', $ref));
|
||||
|
||||
$sync_ref = sprintf(
|
||||
'refs/remotes/%s/%s',
|
||||
$this->getTargetRemote(),
|
||||
$this->getTargetOnto());
|
||||
|
||||
$err = $api->execPassthru(
|
||||
'p4 sync --silent --branch %R --',
|
||||
$sync_ref);
|
||||
|
||||
if ($err) {
|
||||
throw new ArcanistUsageException(
|
||||
pht(
|
||||
'Perforce sync failed! Fix the error and run "arc land" again.'));
|
||||
}
|
||||
} else {
|
||||
$this->writeInfo(
|
||||
pht('FETCH'),
|
||||
pht('Fetching "%s"...', $ref));
|
||||
|
||||
$err = $api->execPassthru(
|
||||
'fetch --quiet -- %s %s',
|
||||
$this->getTargetRemote(),
|
||||
$this->getTargetOnto());
|
||||
|
||||
if ($err) {
|
||||
throw new ArcanistUsageException(
|
||||
pht(
|
||||
'Fetch failed! Fix the error and run "arc land" again.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function updateWorkingCopy() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
$source = $this->sourceCommit;
|
||||
|
||||
$api->execxLocal(
|
||||
'checkout %s --',
|
||||
$this->getTargetFullRef());
|
||||
|
||||
list($original_author, $original_date) = $this->getAuthorAndDate($source);
|
||||
|
||||
try {
|
||||
if ($this->getShouldSquash()) {
|
||||
// NOTE: We're explicitly specifying "--ff" to override the presence
|
||||
// of "merge.ff" options in user configuration.
|
||||
|
||||
$api->execxLocal(
|
||||
'merge --no-stat --no-commit --ff --squash -- %s',
|
||||
$source);
|
||||
} else {
|
||||
$api->execxLocal(
|
||||
'merge --no-stat --no-commit --no-ff -- %s',
|
||||
$source);
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$api->execManualLocal('merge --abort');
|
||||
$api->execManualLocal('reset --hard HEAD --');
|
||||
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Local "%s" does not merge cleanly into "%s". Merge or rebase '.
|
||||
'local changes so they can merge cleanly.',
|
||||
$this->getSourceRef(),
|
||||
$this->getTargetFullRef()));
|
||||
}
|
||||
|
||||
// TODO: This could probably be cleaner by asking the API a question
|
||||
// about working copy status instead of running a raw diff command. See
|
||||
// discussion in T11435.
|
||||
list($changes) = $api->execxLocal('diff --no-ext-diff HEAD --');
|
||||
$changes = trim($changes);
|
||||
if (!strlen($changes)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Merging local "%s" into "%s" produces an empty diff. '.
|
||||
'This usually means these changes have already landed.',
|
||||
$this->getSourceRef(),
|
||||
$this->getTargetFullRef()));
|
||||
}
|
||||
|
||||
$api->execxLocal(
|
||||
'commit --author %s --date %s -F %s --',
|
||||
$original_author,
|
||||
$original_date,
|
||||
$this->getCommitMessageFile());
|
||||
|
||||
$this->getWorkflow()->didCommitMerge();
|
||||
|
||||
list($stdout) = $api->execxLocal(
|
||||
'rev-parse --verify %s',
|
||||
'HEAD');
|
||||
$this->mergedRef = trim($stdout);
|
||||
}
|
||||
|
||||
private function pushChange() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
if ($this->getIsGitPerforce()) {
|
||||
$this->writeInfo(
|
||||
pht('SUBMITTING'),
|
||||
pht('Submitting changes to "%s".', $this->getTargetFullRef()));
|
||||
|
||||
$config_argv = array();
|
||||
|
||||
// Skip the "git p4 submit" interactive editor workflow. We expect
|
||||
// the commit message that "arc land" has built to be satisfactory.
|
||||
$config_argv[] = '-c';
|
||||
$config_argv[] = 'git-p4.skipSubmitEdit=true';
|
||||
|
||||
// Skip the "git p4 submit" confirmation prompt if the user does not edit
|
||||
// the submit message.
|
||||
$config_argv[] = '-c';
|
||||
$config_argv[] = 'git-p4.skipSubmitEditCheck=true';
|
||||
|
||||
$flags_argv = array();
|
||||
|
||||
// Disable implicit "git p4 rebase" as part of submit. We're allowing
|
||||
// the implicit "git p4 sync" to go through since this puts us in a
|
||||
// state which is generally similar to the state after "git push", with
|
||||
// updated remotes.
|
||||
|
||||
// We could do a manual "git p4 sync" with a more narrow "--branch"
|
||||
// instead, but it's not clear that this is beneficial.
|
||||
$flags_argv[] = '--disable-rebase';
|
||||
|
||||
// Detect moves and submit them to Perforce as move operations.
|
||||
$flags_argv[] = '-M';
|
||||
|
||||
// If we run into a conflict, abort the operation. We expect users to
|
||||
// fix conflicts and run "arc land" again.
|
||||
$flags_argv[] = '--conflict=quit';
|
||||
|
||||
$err = $api->execPassthru(
|
||||
'%LR p4 submit %LR --commit %R --',
|
||||
$config_argv,
|
||||
$flags_argv,
|
||||
$this->mergedRef);
|
||||
|
||||
if ($err) {
|
||||
throw new ArcanistUsageException(
|
||||
pht(
|
||||
'Submit failed! Fix the error and run "arc land" again.'));
|
||||
}
|
||||
} else {
|
||||
$this->writeInfo(
|
||||
pht('PUSHING'),
|
||||
pht('Pushing changes to "%s".', $this->getTargetFullRef()));
|
||||
|
||||
$err = $api->execPassthru(
|
||||
'push -- %s %s:%s',
|
||||
$this->getTargetRemote(),
|
||||
$this->mergedRef,
|
||||
$this->getTargetOnto());
|
||||
|
||||
if ($err) {
|
||||
throw new ArcanistUsageException(
|
||||
pht(
|
||||
'Push failed! Fix the error and run "arc land" again.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function reconcileLocalState() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
// Try to put the user into the best final state we can. This is very
|
||||
// complicated because users are incredibly creative and their local
|
||||
// branches may have the same names as branches in the remote but no
|
||||
// relationship to them.
|
||||
|
||||
if ($this->localRef != $this->getSourceRef()) {
|
||||
// The user ran `arc land X` but was on a different branch, so just put
|
||||
// them back wherever they were before.
|
||||
$this->writeInfo(
|
||||
pht('RESTORE'),
|
||||
pht('Switching back to "%s".', $this->localRef));
|
||||
$this->restoreLocalState();
|
||||
return;
|
||||
}
|
||||
|
||||
// We're going to try to find a path to the upstream target branch. We
|
||||
// try in two different ways:
|
||||
//
|
||||
// - follow the source branch directly along tracking branches until
|
||||
// we reach the upstream; or
|
||||
// - follow a local branch with the same name as the target branch until
|
||||
// we reach the upstream.
|
||||
|
||||
// First, get the path from whatever we landed to wherever it goes.
|
||||
$local_branch = $this->getSourceRef();
|
||||
|
||||
$path = $api->getPathToUpstream($local_branch);
|
||||
if ($path->getLength()) {
|
||||
// We may want to discard the thing we landed from the path, if we're
|
||||
// going to delete it. In this case, we don't want to update it or worry
|
||||
// if it's dirty.
|
||||
if ($this->getSourceRef() == $this->getTargetOnto()) {
|
||||
// In this case, we've done something like land "master" onto itself,
|
||||
// so we do want to update the actual branch. We're going to use the
|
||||
// entire path.
|
||||
} else {
|
||||
// Otherwise, we're going to delete the branch at the end of the
|
||||
// workflow, so throw it away the most-local branch that isn't long
|
||||
// for this world.
|
||||
$path->removeUpstream($local_branch);
|
||||
|
||||
if (!$path->getLength()) {
|
||||
// The local branch tracked upstream directly; however, it
|
||||
// may not be the only one to do so. If there's a local
|
||||
// branch of the same name that tracks the remote, try
|
||||
// switching to that.
|
||||
$local_branch = $this->getTargetOnto();
|
||||
list($err) = $api->execManualLocal(
|
||||
'rev-parse --verify %s',
|
||||
$local_branch);
|
||||
if (!$err) {
|
||||
$path = $api->getPathToUpstream($local_branch);
|
||||
}
|
||||
if (!$path->isConnectedToRemote()) {
|
||||
$this->writeInfo(
|
||||
pht('UPDATE'),
|
||||
pht(
|
||||
'Local branch "%s" directly tracks remote, staying on '.
|
||||
'detached HEAD.',
|
||||
$local_branch));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$local_branch = head($path->getLocalBranches());
|
||||
}
|
||||
} else {
|
||||
// The source branch has no upstream, so look for a local branch with
|
||||
// the same name as the target branch. This corresponds to the common
|
||||
// case where you have "master" and checkout local branches from it
|
||||
// with "git checkout -b feature", then land onto "master".
|
||||
|
||||
$local_branch = $this->getTargetOnto();
|
||||
|
||||
list($err) = $api->execManualLocal(
|
||||
'rev-parse --verify %s',
|
||||
$local_branch);
|
||||
if ($err) {
|
||||
$this->writeInfo(
|
||||
pht('UPDATE'),
|
||||
pht(
|
||||
'Local branch "%s" does not exist, staying on detached HEAD.',
|
||||
$local_branch));
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $api->getPathToUpstream($local_branch);
|
||||
}
|
||||
|
||||
if ($path->getCycle()) {
|
||||
$this->writeWarn(
|
||||
pht('LOCAL CYCLE'),
|
||||
pht(
|
||||
'Local branch "%s" tracks an upstream but following it leads to '.
|
||||
'a local cycle, staying on detached HEAD.',
|
||||
$local_branch));
|
||||
return;
|
||||
}
|
||||
|
||||
$is_perforce = $this->getIsGitPerforce();
|
||||
|
||||
if ($is_perforce) {
|
||||
// If we're in Perforce mode, we don't expect to have a meaningful
|
||||
// path to the remote: the "p4" remote is not a real remote, and
|
||||
// "git p4" commands do not configure branch upstreams to provide
|
||||
// a path.
|
||||
|
||||
// Just pretend the target branch is connected directly to the remote,
|
||||
// since this is effectively the behavior of Perforce and appears to
|
||||
// do the right thing.
|
||||
$cascade_branches = array($local_branch);
|
||||
} else {
|
||||
if (!$path->isConnectedToRemote()) {
|
||||
$this->writeInfo(
|
||||
pht('UPDATE'),
|
||||
pht(
|
||||
'Local branch "%s" is not connected to a remote, staying on '.
|
||||
'detached HEAD.',
|
||||
$local_branch));
|
||||
return;
|
||||
}
|
||||
|
||||
$remote_remote = $path->getRemoteRemoteName();
|
||||
$remote_branch = $path->getRemoteBranchName();
|
||||
|
||||
$remote_actual = $remote_remote.'/'.$remote_branch;
|
||||
$remote_expect = $this->getTargetFullRef();
|
||||
if ($remote_actual != $remote_expect) {
|
||||
$this->writeInfo(
|
||||
pht('UPDATE'),
|
||||
pht(
|
||||
'Local branch "%s" is connected to a remote ("%s") other than '.
|
||||
'the target remote ("%s"), staying on detached HEAD.',
|
||||
$local_branch,
|
||||
$remote_actual,
|
||||
$remote_expect));
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get this far, we have a sequence of branches which ultimately
|
||||
// connect to the remote. We're going to try to update them all in reverse
|
||||
// order, from most-upstream to most-local.
|
||||
|
||||
$cascade_branches = $path->getLocalBranches();
|
||||
$cascade_branches = array_reverse($cascade_branches);
|
||||
}
|
||||
|
||||
// First, check if any of them are ahead of the remote.
|
||||
|
||||
$ahead_of_remote = array();
|
||||
foreach ($cascade_branches as $cascade_branch) {
|
||||
list($stdout) = $api->execxLocal(
|
||||
'log %s..%s --',
|
||||
$this->mergedRef,
|
||||
$cascade_branch);
|
||||
$stdout = trim($stdout);
|
||||
|
||||
if (strlen($stdout)) {
|
||||
$ahead_of_remote[$cascade_branch] = $cascade_branch;
|
||||
}
|
||||
}
|
||||
|
||||
// We're going to handle the last branch (the thing we ultimately intend
|
||||
// to check out) differently. It's OK if it's ahead of the remote, as long
|
||||
// as we just landed it.
|
||||
|
||||
$local_ahead = isset($ahead_of_remote[$local_branch]);
|
||||
unset($ahead_of_remote[$local_branch]);
|
||||
$land_self = ($this->getTargetOnto() === $this->getSourceRef());
|
||||
|
||||
// We aren't going to pull anything if anything upstream from us is ahead
|
||||
// of the remote, or the local is ahead of the remote and we didn't land
|
||||
// it onto itself.
|
||||
$skip_pull = ($ahead_of_remote || ($local_ahead && !$land_self));
|
||||
|
||||
if ($skip_pull) {
|
||||
$this->writeInfo(
|
||||
pht('UPDATE'),
|
||||
pht(
|
||||
'Local "%s" is ahead of remote "%s". Checking out "%s" but '.
|
||||
'not pulling changes.',
|
||||
nonempty(head($ahead_of_remote), $local_branch),
|
||||
$this->getTargetFullRef(),
|
||||
$local_branch));
|
||||
|
||||
$this->writeInfo(
|
||||
pht('CHECKOUT'),
|
||||
pht(
|
||||
'Checking out "%s".',
|
||||
$local_branch));
|
||||
|
||||
$api->execxLocal('checkout %s --', $local_branch);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If nothing upstream from our nearest branch is ahead of the remote,
|
||||
// pull it all.
|
||||
|
||||
$cascade_targets = array();
|
||||
if (!$ahead_of_remote) {
|
||||
foreach ($cascade_branches as $cascade_branch) {
|
||||
if ($local_ahead && ($local_branch == $cascade_branch)) {
|
||||
continue;
|
||||
}
|
||||
$cascade_targets[] = $cascade_branch;
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_perforce) {
|
||||
// In Perforce, we've already set the remote to the right state with an
|
||||
// implicit "git p4 sync" during "git p4 submit", and "git pull" isn't a
|
||||
// meaningful operation. We're going to skip this step and jump down to
|
||||
// the "git reset --hard" below to get everything into the right state.
|
||||
} else if ($cascade_targets) {
|
||||
$this->writeInfo(
|
||||
pht('UPDATE'),
|
||||
pht(
|
||||
'Local "%s" tracks target remote "%s", checking out and '.
|
||||
'pulling changes.',
|
||||
$local_branch,
|
||||
$this->getTargetFullRef()));
|
||||
|
||||
foreach ($cascade_targets as $cascade_branch) {
|
||||
$this->writeInfo(
|
||||
pht('PULL'),
|
||||
pht(
|
||||
'Checking out and pulling "%s".',
|
||||
$cascade_branch));
|
||||
|
||||
$api->execxLocal('checkout %s --', $cascade_branch);
|
||||
$api->execxLocal(
|
||||
'pull %s %s --',
|
||||
$this->getTargetRemote(),
|
||||
$cascade_branch);
|
||||
}
|
||||
|
||||
if (!$local_ahead) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// In this case, the user did something like land a branch onto itself,
|
||||
// and the branch is tracking the correct remote. We're going to discard
|
||||
// the local state and reset it to the state we just pushed.
|
||||
|
||||
$this->writeInfo(
|
||||
pht('RESET'),
|
||||
pht(
|
||||
'Local "%s" landed into remote "%s", resetting local branch to '.
|
||||
'remote state.',
|
||||
$this->getTargetOnto(),
|
||||
$this->getTargetFullRef()));
|
||||
|
||||
$api->execxLocal('checkout %s --', $local_branch);
|
||||
$api->execxLocal('reset --hard %s --', $this->getTargetFullRef());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private function destroyLocalBranch() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
$source_ref = $this->getSourceRef();
|
||||
|
||||
if ($source_ref == $this->getTargetOnto()) {
|
||||
// If we landed a branch into a branch with the same name, so don't
|
||||
// destroy it. This prevents us from cleaning up "master" if you're
|
||||
// landing master into itself.
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Maybe this should also recover the proper upstream?
|
||||
|
||||
// See T10321. If we were not landing a branch, don't try to clean it up.
|
||||
// This happens most often when landing from a detached HEAD.
|
||||
$is_branch = $this->isBranch($source_ref);
|
||||
if (!$is_branch) {
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht(
|
||||
'(Source "%s" is not a branch, leaving working copy as-is.)',
|
||||
$source_ref));
|
||||
return;
|
||||
}
|
||||
|
||||
$recovery_command = csprintf(
|
||||
'git checkout -b %R %R',
|
||||
$source_ref,
|
||||
$this->sourceCommit);
|
||||
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht('Cleaning up branch "%s"...', $source_ref));
|
||||
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht('(Use `%s` if you want it back.)', $recovery_command));
|
||||
|
||||
$api->execxLocal('branch -D -- %s', $source_ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the local working copy state so we can restore it later.
|
||||
*/
|
||||
private function saveLocalState() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
$this->localCommit = $api->getWorkingCopyRevision();
|
||||
|
||||
list($ref) = $api->execxLocal('rev-parse --abbrev-ref HEAD');
|
||||
$ref = trim($ref);
|
||||
if ($ref === 'HEAD') {
|
||||
$ref = $this->localCommit;
|
||||
}
|
||||
|
||||
$this->localRef = $ref;
|
||||
|
||||
$this->restoreWhenDestroyed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the working copy to the state it was in before we started
|
||||
* performing writes.
|
||||
*/
|
||||
private function restoreLocalState() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
$api->execxLocal('checkout %s --', $this->localRef);
|
||||
$api->execxLocal('reset --hard %s --', $this->localCommit);
|
||||
$api->execxLocal('submodule update --init --recursive');
|
||||
|
||||
$this->restoreWhenDestroyed = false;
|
||||
}
|
||||
|
||||
private function getTargetFullRef() {
|
||||
return $this->getTargetRemote().'/'.$this->getTargetOnto();
|
||||
}
|
||||
|
||||
private function getAuthorAndDate($commit) {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
// TODO: This is working around Windows escaping problems, see T8298.
|
||||
|
||||
list($info) = $api->execxLocal(
|
||||
'log -n1 --format=%C %s --',
|
||||
'%aD%n%an%n%ae',
|
||||
$commit);
|
||||
|
||||
$info = trim($info);
|
||||
list($date, $author, $email) = explode("\n", $info, 3);
|
||||
|
||||
return array(
|
||||
"$author <{$email}>",
|
||||
$date,
|
||||
);
|
||||
}
|
||||
|
||||
private function didHoldChanges() {
|
||||
if ($this->getIsGitPerforce()) {
|
||||
$this->writeInfo(
|
||||
pht('HOLD'),
|
||||
pht(
|
||||
'Holding change locally, it has not been submitted.'));
|
||||
|
||||
$push_command = csprintf(
|
||||
'$ git p4 submit -M --commit %R --',
|
||||
$this->mergedRef);
|
||||
} else {
|
||||
$this->writeInfo(
|
||||
pht('HOLD'),
|
||||
pht(
|
||||
'Holding change locally, it has not been pushed.'));
|
||||
|
||||
$push_command = csprintf(
|
||||
'$ git push -- %R %R:%R',
|
||||
$this->getTargetRemote(),
|
||||
$this->mergedRef,
|
||||
$this->getTargetOnto());
|
||||
}
|
||||
|
||||
$restore_command = csprintf(
|
||||
'$ git checkout %R --',
|
||||
$this->localRef);
|
||||
|
||||
echo tsprintf(
|
||||
"\n%s\n\n".
|
||||
"%s\n\n".
|
||||
" **%s**\n\n".
|
||||
"%s\n\n".
|
||||
" **%s**\n\n".
|
||||
"%s\n",
|
||||
pht(
|
||||
'This local working copy now contains the merged changes in a '.
|
||||
'detached state.'),
|
||||
pht('You can push the changes manually with this command:'),
|
||||
$push_command,
|
||||
pht(
|
||||
'You can go back to how things were before you ran "arc land" with '.
|
||||
'this command:'),
|
||||
$restore_command,
|
||||
pht(
|
||||
'Local branches have not been changed, and are still in exactly the '.
|
||||
'same state as before.'));
|
||||
}
|
||||
|
||||
private function isBranch($ref) {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
list($err) = $api->execManualLocal(
|
||||
'show-ref --verify --quiet -- %R',
|
||||
'refs/heads/'.$ref);
|
||||
|
||||
return !$err;
|
||||
}
|
||||
|
||||
private function getEngineOnto() {
|
||||
$source_ref = $this->getSourceRef();
|
||||
|
||||
$onto = $this->getOntoArgument();
|
||||
if ($onto !== null) {
|
||||
$this->writeInfo(
|
||||
pht('TARGET'),
|
||||
pht(
|
||||
'Landing onto "%s", selected with the "--onto" flag.',
|
||||
$onto));
|
||||
return $onto;
|
||||
}
|
||||
|
||||
$api = $this->getRepositoryAPI();
|
||||
$path = $api->getPathToUpstream($source_ref);
|
||||
|
||||
if ($path->getLength()) {
|
||||
$cycle = $path->getCycle();
|
||||
if ($cycle) {
|
||||
$this->writeWarn(
|
||||
pht('LOCAL CYCLE'),
|
||||
pht(
|
||||
'Local branch tracks an upstream, but following it leads to a '.
|
||||
'local cycle; ignoring branch upstream.'));
|
||||
|
||||
echo tsprintf(
|
||||
"\n %s\n\n",
|
||||
implode(' -> ', $cycle));
|
||||
|
||||
} else {
|
||||
if ($path->isConnectedToRemote()) {
|
||||
$onto = $path->getRemoteBranchName();
|
||||
$this->writeInfo(
|
||||
pht('TARGET'),
|
||||
pht(
|
||||
'Landing onto "%s", selected by following tracking branches '.
|
||||
'upstream to the closest remote.',
|
||||
$onto));
|
||||
return $onto;
|
||||
} else {
|
||||
$this->writeInfo(
|
||||
pht('NO PATH TO UPSTREAM'),
|
||||
pht(
|
||||
'Local branch tracks an upstream, but there is no path '.
|
||||
'to a remote; ignoring branch upstream.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$workflow = $this->getWorkflow();
|
||||
|
||||
$config_key = 'arc.land.onto.default';
|
||||
$onto = $workflow->getConfigFromAnySource($config_key);
|
||||
if ($onto !== null) {
|
||||
$this->writeInfo(
|
||||
pht('TARGET'),
|
||||
pht(
|
||||
'Landing onto "%s", selected by "%s" configuration.',
|
||||
$onto,
|
||||
$config_key));
|
||||
return $onto;
|
||||
}
|
||||
|
||||
$onto = 'master';
|
||||
$this->writeInfo(
|
||||
pht('TARGET'),
|
||||
pht(
|
||||
'Landing onto "%s", the default target under git.',
|
||||
$onto));
|
||||
|
||||
return $onto;
|
||||
}
|
||||
|
||||
private function getEngineRemote() {
|
||||
$source_ref = $this->getSourceRef();
|
||||
|
||||
$remote = $this->getRemoteArgument();
|
||||
if ($remote !== null) {
|
||||
$this->writeInfo(
|
||||
pht('REMOTE'),
|
||||
pht(
|
||||
'Using remote "%s", selected with the "--remote" flag.',
|
||||
$remote));
|
||||
return $remote;
|
||||
}
|
||||
|
||||
$api = $this->getRepositoryAPI();
|
||||
$path = $api->getPathToUpstream($source_ref);
|
||||
|
||||
$remote = $path->getRemoteRemoteName();
|
||||
if ($remote !== null) {
|
||||
$this->writeInfo(
|
||||
pht('REMOTE'),
|
||||
pht(
|
||||
'Using remote "%s", selected by following tracking branches '.
|
||||
'upstream to the closest remote.',
|
||||
$remote));
|
||||
return $remote;
|
||||
}
|
||||
|
||||
$remote = 'p4';
|
||||
if ($api->isPerforceRemote($remote)) {
|
||||
$this->writeInfo(
|
||||
pht('REMOTE'),
|
||||
pht(
|
||||
'Using Perforce remote "%s". The existence of this remote implies '.
|
||||
'this working copy was synchronized from a Perforce repository.',
|
||||
$remote));
|
||||
return $remote;
|
||||
}
|
||||
|
||||
$remote = 'origin';
|
||||
$this->writeInfo(
|
||||
pht('REMOTE'),
|
||||
pht(
|
||||
'Using remote "%s", the default remote under Git.',
|
||||
$remote));
|
||||
|
||||
return $remote;
|
||||
}
|
||||
|
||||
}
|
149
src/land/ArcanistLandCommit.php
Normal file
149
src/land/ArcanistLandCommit.php
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistLandCommit
|
||||
extends Phobject {
|
||||
|
||||
private $hash;
|
||||
private $summary;
|
||||
private $displaySummary;
|
||||
private $parents;
|
||||
private $symbols = array();
|
||||
private $explicitRevisionRef;
|
||||
private $revisionRef = false;
|
||||
private $parentCommits;
|
||||
private $isHeadCommit;
|
||||
private $isImplicitCommit;
|
||||
|
||||
public function setHash($hash) {
|
||||
$this->hash = $hash;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHash() {
|
||||
return $this->hash;
|
||||
}
|
||||
|
||||
public function setSummary($summary) {
|
||||
$this->summary = $summary;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSummary() {
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
public function getDisplaySummary() {
|
||||
if ($this->displaySummary === null) {
|
||||
$this->displaySummary = id(new PhutilUTF8StringTruncator())
|
||||
->setMaximumGlyphs(64)
|
||||
->truncateString($this->getSummary());
|
||||
}
|
||||
return $this->displaySummary;
|
||||
}
|
||||
|
||||
public function setParents(array $parents) {
|
||||
$this->parents = $parents;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParents() {
|
||||
return $this->parents;
|
||||
}
|
||||
|
||||
public function addSymbol(ArcanistLandSymbol $symbol) {
|
||||
$this->symbols[] = $symbol;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSymbols() {
|
||||
return $this->symbols;
|
||||
}
|
||||
|
||||
public function setExplicitRevisionref(ArcanistRevisionRef $ref) {
|
||||
$this->explicitRevisionRef = $ref;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExplicitRevisionref() {
|
||||
return $this->explicitRevisionRef;
|
||||
}
|
||||
|
||||
public function setParentCommits(array $parent_commits) {
|
||||
$this->parentCommits = $parent_commits;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentCommits() {
|
||||
return $this->parentCommits;
|
||||
}
|
||||
|
||||
public function setIsHeadCommit($is_head_commit) {
|
||||
$this->isHeadCommit = $is_head_commit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsHeadCommit() {
|
||||
return $this->isHeadCommit;
|
||||
}
|
||||
|
||||
public function setIsImplicitCommit($is_implicit_commit) {
|
||||
$this->isImplicitCommit = $is_implicit_commit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsImplicitCommit() {
|
||||
return $this->isImplicitCommit;
|
||||
}
|
||||
|
||||
public function getAncestorRevisionPHIDs() {
|
||||
$phids = array();
|
||||
|
||||
foreach ($this->getParentCommits() as $parent_commit) {
|
||||
$phids += $parent_commit->getAncestorRevisionPHIDs();
|
||||
}
|
||||
|
||||
$revision_ref = $this->getRevisionRef();
|
||||
if ($revision_ref) {
|
||||
$phids[$revision_ref->getPHID()] = $revision_ref->getPHID();
|
||||
}
|
||||
|
||||
return $phids;
|
||||
}
|
||||
|
||||
public function getRevisionRef() {
|
||||
if ($this->revisionRef === false) {
|
||||
$this->revisionRef = $this->newRevisionRef();
|
||||
}
|
||||
|
||||
return $this->revisionRef;
|
||||
}
|
||||
|
||||
private function newRevisionRef() {
|
||||
$revision_ref = $this->getExplicitRevisionRef();
|
||||
if ($revision_ref) {
|
||||
return $revision_ref;
|
||||
}
|
||||
|
||||
$parent_refs = array();
|
||||
foreach ($this->getParentCommits() as $parent_commit) {
|
||||
$parent_ref = $parent_commit->getRevisionRef();
|
||||
if ($parent_ref) {
|
||||
$parent_refs[$parent_ref->getPHID()] = $parent_ref;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($parent_refs) > 1) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Too many distinct parent refs!'));
|
||||
}
|
||||
|
||||
if ($parent_refs) {
|
||||
return head($parent_refs);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
52
src/land/ArcanistLandCommitSet.php
Normal file
52
src/land/ArcanistLandCommitSet.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistLandCommitSet
|
||||
extends Phobject {
|
||||
|
||||
private $revisionRef;
|
||||
private $commits;
|
||||
|
||||
public function setRevisionRef(ArcanistRevisionRef $revision_ref) {
|
||||
$this->revisionRef = $revision_ref;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRevisionRef() {
|
||||
return $this->revisionRef;
|
||||
}
|
||||
|
||||
public function setCommits(array $commits) {
|
||||
assert_instances_of($commits, 'ArcanistLandCommit');
|
||||
$this->commits = $commits;
|
||||
|
||||
$revision_phid = $this->getRevisionRef()->getPHID();
|
||||
foreach ($commits as $commit) {
|
||||
$revision_ref = $commit->getExplicitRevisionRef();
|
||||
|
||||
if ($revision_ref) {
|
||||
if ($revision_ref->getPHID() === $revision_phid) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$commit->setIsImplicitCommit(true);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCommits() {
|
||||
return $this->commits;
|
||||
}
|
||||
|
||||
public function hasImplicitCommits() {
|
||||
foreach ($this->commits as $commit) {
|
||||
if ($commit->getIsImplicitCommit()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
<?php
|
||||
|
||||
abstract class ArcanistLandEngine extends Phobject {
|
||||
|
||||
private $workflow;
|
||||
private $repositoryAPI;
|
||||
private $targetRemote;
|
||||
private $targetOnto;
|
||||
private $sourceRef;
|
||||
private $commitMessageFile;
|
||||
private $shouldHold;
|
||||
private $shouldKeep;
|
||||
private $shouldSquash;
|
||||
private $shouldDeleteRemote;
|
||||
private $shouldPreview;
|
||||
private $remoteArgument;
|
||||
private $ontoArgument;
|
||||
|
||||
// TODO: This is really grotesque.
|
||||
private $buildMessageCallback;
|
||||
|
||||
final public function setWorkflow(ArcanistWorkflow $workflow) {
|
||||
$this->workflow = $workflow;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getWorkflow() {
|
||||
return $this->workflow;
|
||||
}
|
||||
|
||||
final public function setRepositoryAPI(
|
||||
ArcanistRepositoryAPI $repository_api) {
|
||||
$this->repositoryAPI = $repository_api;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getRepositoryAPI() {
|
||||
return $this->repositoryAPI;
|
||||
}
|
||||
|
||||
final public function setShouldHold($should_hold) {
|
||||
$this->shouldHold = $should_hold;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getShouldHold() {
|
||||
return $this->shouldHold;
|
||||
}
|
||||
|
||||
final public function setShouldKeep($should_keep) {
|
||||
$this->shouldKeep = $should_keep;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getShouldKeep() {
|
||||
return $this->shouldKeep;
|
||||
}
|
||||
|
||||
final public function setShouldSquash($should_squash) {
|
||||
$this->shouldSquash = $should_squash;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getShouldSquash() {
|
||||
return $this->shouldSquash;
|
||||
}
|
||||
|
||||
final public function setShouldPreview($should_preview) {
|
||||
$this->shouldPreview = $should_preview;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getShouldPreview() {
|
||||
return $this->shouldPreview;
|
||||
}
|
||||
|
||||
final public function setTargetRemote($target_remote) {
|
||||
$this->targetRemote = $target_remote;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getTargetRemote() {
|
||||
return $this->targetRemote;
|
||||
}
|
||||
|
||||
final public function setTargetOnto($target_onto) {
|
||||
$this->targetOnto = $target_onto;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getTargetOnto() {
|
||||
return $this->targetOnto;
|
||||
}
|
||||
|
||||
final public function setSourceRef($source_ref) {
|
||||
$this->sourceRef = $source_ref;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getSourceRef() {
|
||||
return $this->sourceRef;
|
||||
}
|
||||
|
||||
final public function setBuildMessageCallback($build_message_callback) {
|
||||
$this->buildMessageCallback = $build_message_callback;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getBuildMessageCallback() {
|
||||
return $this->buildMessageCallback;
|
||||
}
|
||||
|
||||
final public function setCommitMessageFile($commit_message_file) {
|
||||
$this->commitMessageFile = $commit_message_file;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getCommitMessageFile() {
|
||||
return $this->commitMessageFile;
|
||||
}
|
||||
|
||||
final public function setRemoteArgument($remote_argument) {
|
||||
$this->remoteArgument = $remote_argument;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getRemoteArgument() {
|
||||
return $this->remoteArgument;
|
||||
}
|
||||
|
||||
final public function setOntoArgument($onto_argument) {
|
||||
$this->ontoArgument = $onto_argument;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getOntoArgument() {
|
||||
return $this->ontoArgument;
|
||||
}
|
||||
|
||||
abstract public function parseArguments();
|
||||
abstract public function execute();
|
||||
|
||||
abstract protected function getLandingCommits();
|
||||
|
||||
protected function printLandingCommits() {
|
||||
$logs = $this->getLandingCommits();
|
||||
|
||||
if (!$logs) {
|
||||
throw new ArcanistUsageException(
|
||||
pht(
|
||||
'There are no commits on "%s" which are not already present on '.
|
||||
'the target.',
|
||||
$this->getSourceRef()));
|
||||
}
|
||||
|
||||
$list = id(new PhutilConsoleList())
|
||||
->setWrap(false)
|
||||
->addItems($logs);
|
||||
|
||||
id(new PhutilConsoleBlock())
|
||||
->addParagraph(
|
||||
pht(
|
||||
'These %s commit(s) will be landed:',
|
||||
new PhutilNumber(count($logs))))
|
||||
->addList($list)
|
||||
->draw();
|
||||
}
|
||||
|
||||
protected function writeWarn($title, $message) {
|
||||
return $this->getWorkflow()->writeWarn($title, $message);
|
||||
}
|
||||
|
||||
protected function writeInfo($title, $message) {
|
||||
return $this->getWorkflow()->writeInfo($title, $message);
|
||||
}
|
||||
|
||||
protected function writeOkay($title, $message) {
|
||||
return $this->getWorkflow()->writeOkay($title, $message);
|
||||
}
|
||||
|
||||
|
||||
}
|
27
src/land/ArcanistLandSymbol.php
Normal file
27
src/land/ArcanistLandSymbol.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistLandSymbol
|
||||
extends Phobject {
|
||||
|
||||
private $symbol;
|
||||
private $commit;
|
||||
|
||||
public function setSymbol($symbol) {
|
||||
$this->symbol = $symbol;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSymbol() {
|
||||
return $this->symbol;
|
||||
}
|
||||
|
||||
public function setCommit($commit) {
|
||||
$this->commit = $commit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCommit() {
|
||||
return $this->commit;
|
||||
}
|
||||
|
||||
}
|
41
src/land/ArcanistLandTarget.php
Normal file
41
src/land/ArcanistLandTarget.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistLandTarget
|
||||
extends Phobject {
|
||||
|
||||
private $remote;
|
||||
private $ref;
|
||||
private $commit;
|
||||
|
||||
public function setRemote($remote) {
|
||||
$this->remote = $remote;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRemote() {
|
||||
return $this->remote;
|
||||
}
|
||||
|
||||
public function setRef($ref) {
|
||||
$this->ref = $ref;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRef() {
|
||||
return $this->ref;
|
||||
}
|
||||
|
||||
public function getLandTargetKey() {
|
||||
return sprintf('%s/%s', $this->getRemote(), $this->getRef());
|
||||
}
|
||||
|
||||
public function setLandTargetCommit($commit) {
|
||||
$this->commit = $commit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLandTargetCommit() {
|
||||
return $this->commit;
|
||||
}
|
||||
|
||||
}
|
1362
src/land/engine/ArcanistGitLandEngine.php
Normal file
1362
src/land/engine/ArcanistGitLandEngine.php
Normal file
File diff suppressed because it is too large
Load diff
1430
src/land/engine/ArcanistLandEngine.php
Normal file
1430
src/land/engine/ArcanistLandEngine.php
Normal file
File diff suppressed because it is too large
Load diff
603
src/land/engine/ArcanistMercurialLandEngine.php
Normal file
603
src/land/engine/ArcanistMercurialLandEngine.php
Normal file
|
@ -0,0 +1,603 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistMercurialLandEngine
|
||||
extends ArcanistLandEngine {
|
||||
|
||||
protected function getDefaultSymbols() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
$bookmark = $api->getActiveBookmark();
|
||||
if ($bookmark !== null) {
|
||||
|
||||
$log->writeStatus(
|
||||
pht('SOURCE'),
|
||||
pht(
|
||||
'Landing the active bookmark, "%s".',
|
||||
$bookmark));
|
||||
|
||||
return array($bookmark);
|
||||
}
|
||||
|
||||
$branch = $api->getBranchName();
|
||||
if ($branch !== null) {
|
||||
|
||||
$log->writeStatus(
|
||||
pht('SOURCE'),
|
||||
pht(
|
||||
'Landing the current branch, "%s".',
|
||||
$branch));
|
||||
|
||||
return array($branch);
|
||||
}
|
||||
|
||||
throw new Exception(pht('TODO: Operate on raw revision.'));
|
||||
}
|
||||
|
||||
protected function resolveSymbols(array $symbols) {
|
||||
assert_instances_of($symbols, 'ArcanistLandSymbol');
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
foreach ($symbols as $symbol) {
|
||||
$raw_symbol = $symbol->getSymbol();
|
||||
|
||||
if ($api->isBookmark($raw_symbol)) {
|
||||
$hash = $api->getBookmarkCommitHash($raw_symbol);
|
||||
$symbol->setCommit($hash);
|
||||
|
||||
// TODO: Set that this is a bookmark?
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($api->isBranch($raw_symbol)) {
|
||||
$hash = $api->getBranchCommitHash($raw_symbol);
|
||||
$symbol->setCommit($hash);
|
||||
|
||||
// TODO: Set that this is a branch?
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Symbol "%s" is not a bookmark or branch name.',
|
||||
$raw_symbol));
|
||||
}
|
||||
}
|
||||
|
||||
protected function selectOntoRemote(array $symbols) {
|
||||
assert_instances_of($symbols, 'ArcanistLandSymbol');
|
||||
$remote = $this->newOntoRemote($symbols);
|
||||
|
||||
// TODO: Verify this remote actually exists.
|
||||
|
||||
return $remote;
|
||||
}
|
||||
|
||||
private function newOntoRemote(array $symbols) {
|
||||
assert_instances_of($symbols, 'ArcanistLandSymbol');
|
||||
$api = $this->getRepositoryAPI();
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
$remote = $this->getOntoRemoteArgument();
|
||||
if ($remote !== null) {
|
||||
|
||||
$log->writeStatus(
|
||||
pht('ONTO REMOTE'),
|
||||
pht(
|
||||
'Remote "%s" was selected with the "--onto-remote" flag.',
|
||||
$remote));
|
||||
|
||||
return $remote;
|
||||
}
|
||||
|
||||
$remote = $this->getOntoRemoteFromConfiguration();
|
||||
if ($remote !== null) {
|
||||
$remote_key = $this->getOntoRemoteConfigurationKey();
|
||||
|
||||
$log->writeStatus(
|
||||
pht('ONTO REMOTE'),
|
||||
pht(
|
||||
'Remote "%s" was selected by reading "%s" configuration.',
|
||||
$remote,
|
||||
$remote_key));
|
||||
|
||||
return $remote;
|
||||
}
|
||||
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
$default_remote = 'default';
|
||||
|
||||
$log->writeStatus(
|
||||
pht('ONTO REMOTE'),
|
||||
pht(
|
||||
'Landing onto remote "%s", the default remote under Mercurial.',
|
||||
$default_remote));
|
||||
|
||||
return $default_remote;
|
||||
}
|
||||
|
||||
protected function selectOntoRefs(array $symbols) {
|
||||
assert_instances_of($symbols, 'ArcanistLandSymbol');
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
$onto = $this->getOntoArguments();
|
||||
if ($onto) {
|
||||
|
||||
$log->writeStatus(
|
||||
pht('ONTO TARGET'),
|
||||
pht(
|
||||
'Refs were selected with the "--onto" flag: %s.',
|
||||
implode(', ', $onto)));
|
||||
|
||||
return $onto;
|
||||
}
|
||||
|
||||
$onto = $this->getOntoFromConfiguration();
|
||||
if ($onto) {
|
||||
$onto_key = $this->getOntoConfigurationKey();
|
||||
|
||||
$log->writeStatus(
|
||||
pht('ONTO TARGET'),
|
||||
pht(
|
||||
'Refs were selected by reading "%s" configuration: %s.',
|
||||
$onto_key,
|
||||
implode(', ', $onto)));
|
||||
|
||||
return $onto;
|
||||
}
|
||||
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
$default_onto = 'default';
|
||||
|
||||
$log->writeStatus(
|
||||
pht('ONTO TARGET'),
|
||||
pht(
|
||||
'Landing onto target "%s", the default target under Mercurial.',
|
||||
$default_onto));
|
||||
|
||||
return array($default_onto);
|
||||
}
|
||||
|
||||
protected function confirmOntoRefs(array $onto_refs) {
|
||||
foreach ($onto_refs as $onto_ref) {
|
||||
if (!strlen($onto_ref)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Selected "onto" ref "%s" is invalid: the empty string is not '.
|
||||
'a valid ref.',
|
||||
$onto_ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function selectIntoRemote() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
if ($this->getIntoEmptyArgument()) {
|
||||
$this->setIntoEmpty(true);
|
||||
|
||||
$log->writeStatus(
|
||||
pht('INTO REMOTE'),
|
||||
pht(
|
||||
'Will merge into empty state, selected with the "--into-empty" '.
|
||||
'flag.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->getIntoLocalArgument()) {
|
||||
$this->setIntoLocal(true);
|
||||
|
||||
$log->writeStatus(
|
||||
pht('INTO REMOTE'),
|
||||
pht(
|
||||
'Will merge into local state, selected with the "--into-local" '.
|
||||
'flag.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$into = $this->getIntoRemoteArgument();
|
||||
if ($into !== null) {
|
||||
|
||||
// TODO: Verify that this is a valid path.
|
||||
// TODO: Allow a raw URI?
|
||||
|
||||
$this->setIntoRemote($into);
|
||||
|
||||
$log->writeStatus(
|
||||
pht('INTO REMOTE'),
|
||||
pht(
|
||||
'Will merge into remote "%s", selected with the "--into" flag.',
|
||||
$into));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$onto = $this->getOntoRemote();
|
||||
$this->setIntoRemote($onto);
|
||||
|
||||
$log->writeStatus(
|
||||
pht('INTO REMOTE'),
|
||||
pht(
|
||||
'Will merge into remote "%s" by default, because this is the remote '.
|
||||
'the change is landing onto.',
|
||||
$onto));
|
||||
}
|
||||
|
||||
protected function selectIntoRef() {
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
if ($this->getIntoEmptyArgument()) {
|
||||
$log->writeStatus(
|
||||
pht('INTO TARGET'),
|
||||
pht(
|
||||
'Will merge into empty state, selected with the "--into-empty" '.
|
||||
'flag.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$into = $this->getIntoArgument();
|
||||
if ($into !== null) {
|
||||
$this->setIntoRef($into);
|
||||
|
||||
$log->writeStatus(
|
||||
pht('INTO TARGET'),
|
||||
pht(
|
||||
'Will merge into target "%s", selected with the "--into" flag.',
|
||||
$into));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$ontos = $this->getOntoRefs();
|
||||
$onto = head($ontos);
|
||||
|
||||
$this->setIntoRef($onto);
|
||||
if (count($ontos) > 1) {
|
||||
$log->writeStatus(
|
||||
pht('INTO TARGET'),
|
||||
pht(
|
||||
'Will merge into target "%s" by default, because this is the first '.
|
||||
'"onto" target.',
|
||||
$onto));
|
||||
} else {
|
||||
$log->writeStatus(
|
||||
pht('INTO TARGET'),
|
||||
pht(
|
||||
'Will merge into target "%s" by default, because this is the "onto" '.
|
||||
'target.',
|
||||
$onto));
|
||||
}
|
||||
}
|
||||
|
||||
protected function selectIntoCommit() {
|
||||
// Make sure that our "into" target is valid.
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
if ($this->getIntoEmpty()) {
|
||||
// If we're running under "--into-empty", we don't have to do anything.
|
||||
|
||||
$log->writeStatus(
|
||||
pht('INTO COMMIT'),
|
||||
pht('Preparing merge into the empty state.'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->getIntoLocal()) {
|
||||
// If we're running under "--into-local", just make sure that the
|
||||
// target identifies some actual commit.
|
||||
$api = $this->getRepositoryAPI();
|
||||
$local_ref = $this->getIntoRef();
|
||||
|
||||
// TODO: This error handling could probably be cleaner.
|
||||
|
||||
$into_commit = $api->getCanonicalRevisionName($local_ref);
|
||||
|
||||
$log->writeStatus(
|
||||
pht('INTO COMMIT'),
|
||||
pht(
|
||||
'Preparing merge into local target "%s", at commit "%s".',
|
||||
$local_ref,
|
||||
$this->getDisplayHash($into_commit)));
|
||||
|
||||
return $into_commit;
|
||||
}
|
||||
|
||||
$target = id(new ArcanistLandTarget())
|
||||
->setRemote($this->getIntoRemote())
|
||||
->setRef($this->getIntoRef());
|
||||
|
||||
$commit = $this->fetchTarget($target);
|
||||
if ($commit !== null) {
|
||||
$log->writeStatus(
|
||||
pht('INTO COMMIT'),
|
||||
pht(
|
||||
'Preparing merge into "%s" from remote "%s", at commit "%s".',
|
||||
$target->getRef(),
|
||||
$target->getRemote(),
|
||||
$this->getDisplayHash($commit)));
|
||||
return $commit;
|
||||
}
|
||||
|
||||
// If we have no valid target and the user passed "--into" explicitly,
|
||||
// treat this as an error. For example, "arc land --into Q --onto Q",
|
||||
// where "Q" does not exist, is an error.
|
||||
if ($this->getIntoArgument()) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Ref "%s" does not exist in remote "%s".',
|
||||
$target->getRef(),
|
||||
$target->getRemote()));
|
||||
}
|
||||
|
||||
// Otherwise, treat this as implying "--into-empty". For example,
|
||||
// "arc land --onto Q", where "Q" does not exist, is equivalent to
|
||||
// "arc land --into-empty --onto Q".
|
||||
$this->setIntoEmpty(true);
|
||||
|
||||
$log->writeStatus(
|
||||
pht('INTO COMMIT'),
|
||||
pht(
|
||||
'Preparing merge into the empty state to create target "%s" '.
|
||||
'in remote "%s".',
|
||||
$target->getRef(),
|
||||
$target->getRemote()));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function fetchTarget(ArcanistLandTarget $target) {
|
||||
$api = $this->getRepositoryAPI();
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
// TODO: Support bookmarks.
|
||||
// TODO: Deal with bookmark save/restore behavior.
|
||||
// TODO: Format this nicely with passthru.
|
||||
// TODO: Raise a good error message when the ref does not exist.
|
||||
|
||||
$api->execPassthru(
|
||||
'pull -b %s -- %s',
|
||||
$target->getRef(),
|
||||
$target->getRemote());
|
||||
|
||||
// TODO: Deal with multiple branch heads.
|
||||
|
||||
list($stdout) = $api->execxLocal(
|
||||
'log --rev %s --template %s --',
|
||||
hgsprintf(
|
||||
'last(ancestors(%s) and !outgoing(%s))',
|
||||
$target->getRef(),
|
||||
$target->getRemote()),
|
||||
'{node}');
|
||||
|
||||
return trim($stdout);
|
||||
}
|
||||
|
||||
protected function selectCommits($into_commit, array $symbols) {
|
||||
assert_instances_of($symbols, 'ArcanistLandSymbol');
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
$commit_map = array();
|
||||
foreach ($symbols as $symbol) {
|
||||
$symbol_commit = $symbol->getCommit();
|
||||
$template = '{node}-{parents}-';
|
||||
|
||||
if ($into_commit === null) {
|
||||
list($commits) = $api->execxLocal(
|
||||
'log --rev %s --template %s --',
|
||||
hgsprintf('reverse(ancestors(%s))', $into_commit),
|
||||
$template);
|
||||
} else {
|
||||
list($commits) = $api->execxLocal(
|
||||
'log --rev %s --template %s --',
|
||||
hgsprintf(
|
||||
'reverse(ancestors(%s) - ancestors(%s))',
|
||||
$symbol_commit,
|
||||
$into_commit),
|
||||
$template);
|
||||
}
|
||||
|
||||
$commits = phutil_split_lines($commits, false);
|
||||
foreach ($commits as $line) {
|
||||
if (!strlen($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode('-', $line, 3);
|
||||
if (count($parts) < 3) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Unexpected output from "hg log ...": %s',
|
||||
$line));
|
||||
}
|
||||
|
||||
$hash = $parts[0];
|
||||
if (!isset($commit_map[$hash])) {
|
||||
$parents = $parts[1];
|
||||
$parents = trim($parents);
|
||||
if (strlen($parents)) {
|
||||
$parents = explode(' ', $parents);
|
||||
} else {
|
||||
$parents = array();
|
||||
}
|
||||
|
||||
$summary = $parts[2];
|
||||
|
||||
$commit_map[$hash] = id(new ArcanistLandCommit())
|
||||
->setHash($hash)
|
||||
->setParents($parents)
|
||||
->setSummary($summary);
|
||||
}
|
||||
|
||||
$commit = $commit_map[$hash];
|
||||
$commit->addSymbol($symbol);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->confirmCommits($into_commit, $symbols, $commit_map);
|
||||
}
|
||||
|
||||
protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
if ($this->getStrategy() !== 'squash') {
|
||||
throw new Exception(pht('TODO: Support merge strategies'));
|
||||
}
|
||||
|
||||
// TODO: Add a Mercurial version check requiring 2.1.1 or newer.
|
||||
|
||||
$api->execxLocal(
|
||||
'update --rev %s',
|
||||
hgsprintf('%s', $into_commit));
|
||||
|
||||
$commits = $set->getCommits();
|
||||
|
||||
$min_commit = last($commits)->getHash();
|
||||
$max_commit = head($commits)->getHash();
|
||||
|
||||
$revision_ref = $set->getRevisionRef();
|
||||
$commit_message = $revision_ref->getCommitMessage();
|
||||
|
||||
try {
|
||||
$argv = array();
|
||||
$argv[] = '--dest';
|
||||
$argv[] = hgsprintf('%s', $into_commit);
|
||||
|
||||
$argv[] = '--rev';
|
||||
$argv[] = hgsprintf('%s..%s', $min_commit, $max_commit);
|
||||
|
||||
$argv[] = '--logfile';
|
||||
$argv[] = '-';
|
||||
|
||||
$argv[] = '--keep';
|
||||
$argv[] = '--collapse';
|
||||
|
||||
$future = $api->execFutureLocal('rebase %Ls', $argv);
|
||||
$future->write($commit_message);
|
||||
$future->resolvex();
|
||||
|
||||
} catch (CommandException $ex) {
|
||||
// TODO
|
||||
// $api->execManualLocal('rebase --abort');
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}');
|
||||
$new_cursor = trim($stdout);
|
||||
|
||||
return $new_cursor;
|
||||
}
|
||||
|
||||
protected function pushChange($into_commit) {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
// TODO: This does not respect "--into" or "--onto" properly.
|
||||
|
||||
$api->execxLocal(
|
||||
'push --rev %s -- %s',
|
||||
$into_commit,
|
||||
$this->getOntoRemote());
|
||||
}
|
||||
|
||||
protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) {
|
||||
$api = $this->getRepositoryAPI();
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
// This has no effect when we're executing a merge strategy.
|
||||
if (!$this->isSquashStrategy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$old_commit = last($set->getCommits())->getHash();
|
||||
$new_commit = $into_commit;
|
||||
|
||||
list($output) = $api->execxLocal(
|
||||
'log --rev %s --template %s',
|
||||
hgsprintf('children(%s)', $old_commit),
|
||||
'{node}\n');
|
||||
$child_hashes = phutil_split_lines($output, false);
|
||||
|
||||
foreach ($child_hashes as $child_hash) {
|
||||
if (!strlen($child_hash)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: If the only heads which are descendants of this child will
|
||||
// be deleted, we can skip this rebase?
|
||||
|
||||
try {
|
||||
$api->execxLocal(
|
||||
'rebase --source %s --dest %s --keep --keepbranches',
|
||||
$child_hash,
|
||||
$new_commit);
|
||||
} catch (CommandException $ex) {
|
||||
// TODO: Recover state.
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function pruneBranches(array $sets) {
|
||||
assert_instances_of($sets, 'ArcanistLandCommitSet');
|
||||
$api = $this->getRepositoryAPI();
|
||||
$log = $this->getLogEngine();
|
||||
|
||||
// This has no effect when we're executing a merge strategy.
|
||||
if (!$this->isSquashStrategy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$strip = array();
|
||||
|
||||
// We've rebased all descendants already, so we can safely delete all
|
||||
// of these commits.
|
||||
|
||||
$sets = array_reverse($sets);
|
||||
foreach ($sets as $set) {
|
||||
$commits = $set->getCommits();
|
||||
|
||||
$min_commit = head($commits)->getHash();
|
||||
$max_commit = last($commits)->getHash();
|
||||
|
||||
$strip[] = hgsprintf('%s::%s', $min_commit, $max_commit);
|
||||
}
|
||||
|
||||
$rev_set = '('.implode(') or (', $strip).')';
|
||||
|
||||
// See PHI45. If we have "hg evolve", get rid of old commits using
|
||||
// "hg prune" instead of "hg strip".
|
||||
|
||||
// If we "hg strip" a commit which has an obsolete predecessor, it
|
||||
// removes the obsolescence marker and revives the predecessor. This is
|
||||
// not desirable: we want to destroy all predecessors of these commits.
|
||||
|
||||
try {
|
||||
$api->execxLocal(
|
||||
'--config extensions.evolve= prune --rev %s',
|
||||
$rev_set);
|
||||
} catch (CommandException $ex) {
|
||||
$api->execxLocal(
|
||||
'--config extensions.strip= strip --rev %s',
|
||||
$rev_set);
|
||||
}
|
||||
}
|
||||
|
||||
protected function reconcileLocalState(
|
||||
$into_commit,
|
||||
ArcanistRepositoryLocalState $state) {
|
||||
|
||||
// TODO: For now, just leave users wherever they ended up.
|
||||
|
||||
$state->discardLocalState();
|
||||
}
|
||||
|
||||
}
|
36
src/query/ArcanistMercurialCommitMessageHardpointQuery.php
Normal file
36
src/query/ArcanistMercurialCommitMessageHardpointQuery.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistMercurialCommitMessageHardpointQuery
|
||||
extends ArcanistWorkflowMercurialHardpointQuery {
|
||||
|
||||
public function getHardpoints() {
|
||||
return array(
|
||||
ArcanistCommitRef::HARDPOINT_MESSAGE,
|
||||
);
|
||||
}
|
||||
|
||||
protected function canLoadRef(ArcanistRef $ref) {
|
||||
return ($ref instanceof ArcanistCommitRef);
|
||||
}
|
||||
|
||||
public function loadHardpoint(array $refs, $hardpoint) {
|
||||
$api = $this->getRepositoryAPI();
|
||||
|
||||
$hashes = mpull($refs, 'getCommitHash');
|
||||
$unique_hashes = array_fuse($hashes);
|
||||
|
||||
// TODO: Batch this properly and make it future oriented.
|
||||
|
||||
$messages = array();
|
||||
foreach ($unique_hashes as $unique_hash) {
|
||||
$messages[$unique_hash] = $api->getCommitMessage($unique_hash);
|
||||
}
|
||||
|
||||
foreach ($hashes as $ref_key => $hash) {
|
||||
$hashes[$ref_key] = $messages[$hash];
|
||||
}
|
||||
|
||||
yield $this->yieldMap($hashes);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistMercurialWorkingCopyRevisionHardpointQuery
|
||||
extends ArcanistWorkflowMercurialHardpointQuery {
|
||||
|
||||
public function getHardpoints() {
|
||||
return array(
|
||||
ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS,
|
||||
);
|
||||
}
|
||||
|
||||
protected function canLoadRef(ArcanistRef $ref) {
|
||||
return ($ref instanceof ArcanistWorkingCopyStateRef);
|
||||
}
|
||||
|
||||
public function loadHardpoint(array $refs, $hardpoint) {
|
||||
yield $this->yieldRequests(
|
||||
$refs,
|
||||
array(
|
||||
ArcanistWorkingCopyStateRef::HARDPOINT_COMMITREF,
|
||||
));
|
||||
|
||||
// TODO: This has a lot in common with the Git query in the same role.
|
||||
|
||||
$hashes = array();
|
||||
$map = array();
|
||||
foreach ($refs as $ref_key => $ref) {
|
||||
$commit = $ref->getCommitRef();
|
||||
|
||||
$commit_hashes = array();
|
||||
|
||||
$commit_hashes[] = array(
|
||||
'hgcm',
|
||||
$commit->getCommitHash(),
|
||||
);
|
||||
|
||||
foreach ($commit_hashes as $hash) {
|
||||
$hashes[] = $hash;
|
||||
$hash_key = $this->getHashKey($hash);
|
||||
$map[$hash_key][$ref_key] = $ref;
|
||||
}
|
||||
}
|
||||
|
||||
$results = array_fill_keys(array_keys($refs), array());
|
||||
if ($hashes) {
|
||||
$revisions = (yield $this->yieldConduit(
|
||||
'differential.query',
|
||||
array(
|
||||
'commitHashes' => $hashes,
|
||||
)));
|
||||
|
||||
foreach ($revisions as $dict) {
|
||||
$revision_hashes = idx($dict, 'hashes');
|
||||
if (!$revision_hashes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$revision_ref = ArcanistRevisionRef::newFromConduitQuery($dict);
|
||||
foreach ($revision_hashes as $revision_hash) {
|
||||
$hash_key = $this->getHashKey($revision_hash);
|
||||
$state_refs = idx($map, $hash_key, array());
|
||||
foreach ($state_refs as $ref_key => $state_ref) {
|
||||
$results[$ref_key][] = $revision_ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield $this->yieldMap($results);
|
||||
}
|
||||
|
||||
private function getHashKey(array $hash) {
|
||||
return $hash[0].':'.$hash[1];
|
||||
}
|
||||
|
||||
}
|
11
src/query/ArcanistWorkflowMercurialHardpointQuery.php
Normal file
11
src/query/ArcanistWorkflowMercurialHardpointQuery.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
abstract class ArcanistWorkflowMercurialHardpointQuery
|
||||
extends ArcanistRuntimeHardpointQuery {
|
||||
|
||||
final protected function canLoadHardpoint() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
return ($api instanceof ArcanistMercurialAPI);
|
||||
}
|
||||
|
||||
}
|
|
@ -29,7 +29,7 @@ final class ArcanistRevisionBuildableHardpointQuery
|
|||
$buildables = (yield $this->yieldConduitSearch(
|
||||
'harbormaster.buildable.search',
|
||||
array(
|
||||
'objectPHIDs' => $diff_map,
|
||||
'objectPHIDs' => array_values($diff_map),
|
||||
'manual' => false,
|
||||
)));
|
||||
|
||||
|
|
|
@ -1564,6 +1564,11 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
|
|||
return ($uri !== null);
|
||||
}
|
||||
|
||||
public function isFetchableRemote($remote_name) {
|
||||
$uri = $this->getGitRemoteFetchURI($remote_name);
|
||||
return ($uri !== null);
|
||||
}
|
||||
|
||||
private function getGitRemoteFetchURI($remote_name) {
|
||||
return $this->getGitRemoteURI($remote_name, $for_push = false);
|
||||
}
|
||||
|
@ -1739,4 +1744,13 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
|
|||
return false;
|
||||
}
|
||||
|
||||
protected function newLandEngine() {
|
||||
return new ArcanistGitLandEngine();
|
||||
}
|
||||
|
||||
public function newLocalState() {
|
||||
return id(new ArcanistGitLocalState())
|
||||
->setRepositoryAPI($this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -534,6 +534,8 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
}
|
||||
|
||||
public function getAllBranches() {
|
||||
// TODO: This is wrong, and returns bookmarks.
|
||||
|
||||
list($branch_info) = $this->execxLocal('bookmarks');
|
||||
if (trim($branch_info) == 'no bookmarks set') {
|
||||
return array();
|
||||
|
@ -548,10 +550,14 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
|
||||
$return = array();
|
||||
foreach ($matches as $match) {
|
||||
list(, $current, $name) = $match;
|
||||
list(, $current, $name, $hash) = $match;
|
||||
|
||||
list($id, $hash) = explode(':', $hash);
|
||||
|
||||
$return[] = array(
|
||||
'current' => (bool)$current,
|
||||
'name' => rtrim($name),
|
||||
'hash' => $hash,
|
||||
);
|
||||
}
|
||||
return $return;
|
||||
|
@ -562,9 +568,13 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
|
||||
$refs = array();
|
||||
foreach ($branches as $branch) {
|
||||
$commit_ref = $this->newCommitRef()
|
||||
->setCommitHash($branch['hash']);
|
||||
|
||||
$refs[] = $this->newBranchRef()
|
||||
->setBranchName($branch['name'])
|
||||
->setIsCurrentBranch($branch['current']);
|
||||
->setIsCurrentBranch($branch['current'])
|
||||
->attachCommitRef($commit_ref);
|
||||
}
|
||||
|
||||
return $refs;
|
||||
|
@ -1064,6 +1074,46 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
return $bookmarks;
|
||||
}
|
||||
|
||||
public function getBookmarkCommitHash($name) {
|
||||
// TODO: Cache this.
|
||||
|
||||
$bookmarks = $this->getBookmarks($name);
|
||||
$bookmarks = ipull($bookmarks, null, 'name');
|
||||
|
||||
foreach ($bookmarks as $bookmark) {
|
||||
if ($bookmark['name'] === $name) {
|
||||
return $bookmark['revision'];
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception(pht('No bookmark "%s".', $name));
|
||||
}
|
||||
|
||||
public function getBranchCommitHash($name) {
|
||||
// TODO: Cache this.
|
||||
// TODO: This won't work when there are multiple branch heads with the
|
||||
// same name.
|
||||
|
||||
$branches = $this->getBranches($name);
|
||||
|
||||
$heads = array();
|
||||
foreach ($branches as $branch) {
|
||||
if ($branch['name'] === $name) {
|
||||
$heads[] = $branch;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($heads) === 1) {
|
||||
return idx(head($heads), 'revision');
|
||||
}
|
||||
|
||||
if (!$heads) {
|
||||
throw new Exception(pht('No branch "%s".', $name));
|
||||
}
|
||||
|
||||
throw new Exception(pht('Too many branch heads for "%s".', $name));
|
||||
}
|
||||
|
||||
private function splitBranchOrBookmarkLine($line) {
|
||||
// branches and bookmarks are printed in the format:
|
||||
// default 0:a5ead76cdf85 (inactive)
|
||||
|
@ -1108,4 +1158,14 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
return $env;
|
||||
}
|
||||
|
||||
protected function newLandEngine() {
|
||||
return new ArcanistMercurialLandEngine();
|
||||
}
|
||||
|
||||
public function newLocalState() {
|
||||
return id(new ArcanistMercurialLocalState())
|
||||
->setRepositoryAPI($this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -734,4 +734,19 @@ abstract class ArcanistRepositoryAPI extends Phobject {
|
|||
final public function newBranchRef() {
|
||||
return new ArcanistBranchRef();
|
||||
}
|
||||
|
||||
final public function getLandEngine() {
|
||||
$engine = $this->newLandEngine();
|
||||
|
||||
if ($engine) {
|
||||
$engine->setRepositoryAPI($this);
|
||||
}
|
||||
|
||||
return $engine;
|
||||
}
|
||||
|
||||
protected function newLandEngine() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
60
src/repository/state/ArcanistMercurialLocalState.php
Normal file
60
src/repository/state/ArcanistMercurialLocalState.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistMercurialLocalState
|
||||
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();
|
||||
// TODO: Fix this.
|
||||
}
|
||||
|
||||
protected function executeRestoreLocalState() {
|
||||
$api = $this->getRepositoryAPI();
|
||||
// TODO: Fix this.
|
||||
|
||||
// TODO: In Mercurial, we may want to discard commits we've created.
|
||||
// $repository_api->execxLocal(
|
||||
// '--config extensions.mq= strip %s',
|
||||
// $this->onto);
|
||||
|
||||
}
|
||||
|
||||
protected function executeDiscardLocalState() {
|
||||
// TODO: Fix this.
|
||||
}
|
||||
|
||||
protected function canStashChanges() {
|
||||
// Depends on stash extension.
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getIgnoreHints() {
|
||||
// TODO: Provide this.
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function saveStash() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function restoreStash($stash_ref) {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function discardStash($stash_ref) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -151,6 +151,9 @@ abstract class ArcanistRepositoryLocalState
|
|||
$this->executeSaveLocalState();
|
||||
$this->shouldRestore = true;
|
||||
|
||||
// TODO: Detect when we're in the middle of a rebase.
|
||||
// TODO: Detect when we're in the middle of a cherry-pick.
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -244,7 +244,7 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
return $err;
|
||||
}
|
||||
|
||||
final protected function getLogEngine() {
|
||||
final public function getLogEngine() {
|
||||
return $this->getRuntime()->getLogEngine();
|
||||
}
|
||||
|
||||
|
@ -2357,6 +2357,15 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
$prompts = $this->newPrompts();
|
||||
assert_instances_of($prompts, 'ArcanistPrompt');
|
||||
|
||||
$prompts[] = $this->newPrompt('arc.state.stash')
|
||||
->setDescription(
|
||||
pht(
|
||||
'Prompts the user to stash changes and continue when the '.
|
||||
'working copy has untracked, uncommitted. or unstaged '.
|
||||
'changes.'));
|
||||
|
||||
// TODO: Swap to ArrayCheck?
|
||||
|
||||
$map = array();
|
||||
foreach ($prompts as $prompt) {
|
||||
$key = $prompt->getKey();
|
||||
|
@ -2380,7 +2389,7 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
return $this->promptMap;
|
||||
}
|
||||
|
||||
protected function getPrompt($key) {
|
||||
final public function getPrompt($key) {
|
||||
$map = $this->getPromptMap();
|
||||
|
||||
$prompt = idx($map, $key);
|
||||
|
|
Loading…
Reference in a new issue