public function handleMessage(PhutilConsoleMessage $message) { $data = $message->getData(); $type = $message->getType(); switch ($type) { case PhutilConsoleMessage::TYPE_CONFIRM: $ok = phutil_console_confirm($data['prompt'], !$data['default']); return $this->buildMessage(PhutilConsoleMessage::TYPE_INPUT, $ok); case PhutilConsoleMessage::TYPE_PROMPT: $response = phutil_console_prompt($data['prompt'], idx($data, 'history')); return $this->buildMessage(PhutilConsoleMessage::TYPE_INPUT, $response); case PhutilConsoleMessage::TYPE_OUT: $this->writeText(STDOUT, $data); return null; case PhutilConsoleMessage::TYPE_ERR: $this->writeText(STDERR, $data); return null; case PhutilConsoleMessage::TYPE_LOG: if ($this->enableLog) { $this->writeText(STDERR, $data); } return null; default: if ($this->handler) { return call_user_func($this->handler, $message); } else { throw new Exception("Received unknown console message of type '{$type}'."); } } }
/** * @group console */ function phutil_console_confirm($prompt, $default_no = true) { $prompt_options = $default_no ? '[y/N]' : '[Y/n]'; do { $response = phutil_console_prompt($prompt . ' ' . $prompt_options); $c = trim(strtolower($response)); } while ($c != 'y' && $c != 'n' && $c != ''); echo "\n"; if ($default_no) { return $c == 'y'; } else { return $c != 'n'; } }
public function run() { $uri = $this->determineConduitURI(); $this->setConduitURI($uri); $configuration_manager = $this->getConfigurationManager(); echo "Installing certificate for '{$uri}'...\n"; $config = $configuration_manager->readUserConfigurationFile(); echo "Trying to connect to server...\n"; $conduit = $this->establishConduit()->getConduit(); try { $conduit->callMethodSynchronous('conduit.ping', array()); } catch (Exception $ex) { throw new ArcanistUsageException('Failed to connect to server: ' . $ex->getMessage()); } echo "Connection OK!\n"; $token_uri = new PhutilURI($uri); $token_uri->setPath('/conduit/token/'); echo "\n"; echo phutil_console_format("**LOGIN TO PHABRICATOR**\n"); echo "Open this page in your browser and login to Phabricator if " . "necessary:\n"; echo "\n"; echo " {$token_uri}\n"; echo "\n"; echo 'Then paste the token on that page below.'; do { $token = phutil_console_prompt('Paste token from that page:'); $token = trim($token); if (strlen($token)) { break; } } while (true); echo "\n"; echo "Downloading authentication certificate...\n"; $info = $conduit->callMethodSynchronous('conduit.getcertificate', array('token' => $token, 'host' => $uri)); $user = $info['username']; echo "Installing certificate for '{$user}'...\n"; $config['hosts'][$uri] = array('user' => $user, 'cert' => $info['certificate']); echo "Writing ~/.arcrc...\n"; $configuration_manager->writeUserConfigurationFile($config); echo phutil_console_format("<bg:green>** SUCCESS! **</bg> Certificate installed.\n"); return 0; }
public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $console->getServer()->setEnableLog(true); PhabricatorLDAPAuthProvider::assertLDAPExtensionInstalled(); $provider = PhabricatorLDAPAuthProvider::getLDAPProvider(); if (!$provider) { $console->writeOut("%s\n", pht('The LDAP authentication provider is not enabled.')); exit(1); } if (!function_exists('ldap_connect')) { $console->writeOut("%s\n", pht('The LDAP extension is not enabled.')); exit(1); } $adapter = $provider->getAdapter(); $console->writeOut("%s\n", pht('Enter LDAP Credentials')); $username = phutil_console_prompt(pht('LDAP Username: '******'You must enter an LDAP username.')); } phutil_passthru('stty -echo'); $password = phutil_console_prompt(pht('LDAP Password: '******'stty echo'); if (!strlen($password)) { throw new PhutilArgumentUsageException(pht('You must enter an LDAP password.')); } $adapter->setLoginUsername($username); $adapter->setLoginPassword(new PhutilOpaqueEnvelope($password)); $console->writeOut("\n"); $console->writeOut("%s\n", pht('Connecting to LDAP...')); $account_id = $adapter->getAccountID(); if ($account_id) { $console->writeOut("%s\n", pht('Found LDAP Account: %s', $account_id)); } else { $console->writeOut("%s\n", pht('Unable to find LDAP account!')); } return 0; }
public function handleMessage(PhutilConsoleMessage $message) { $data = $message->getData(); $type = $message->getType(); switch ($type) { case PhutilConsoleMessage::TYPE_CONFIRM: $ok = phutil_console_confirm($data['prompt'], !$data['default']); return $this->buildMessage(PhutilConsoleMessage::TYPE_INPUT, $ok); case PhutilConsoleMessage::TYPE_PROMPT: $response = phutil_console_prompt($data['prompt'], idx($data, 'history')); return $this->buildMessage(PhutilConsoleMessage::TYPE_INPUT, $response); case PhutilConsoleMessage::TYPE_OUT: $this->writeText(STDOUT, $data); return null; case PhutilConsoleMessage::TYPE_ERR: $this->writeText(STDERR, $data); return null; case PhutilConsoleMessage::TYPE_LOG: if ($this->enableLog) { $this->writeText(STDERR, $data); } return null; case PhutilConsoleMessage::TYPE_ENABLED: switch ($data['which']) { case PhutilConsoleMessage::TYPE_LOG: $enabled = $this->enableLog; break; default: $enabled = true; break; } return $this->buildMessage(PhutilConsoleMessage::TYPE_IS_ENABLED, $enabled); case PhutilConsoleMessage::TYPE_TTY: case PhutilConsoleMessage::TYPE_COLS: switch ($data['which']) { case PhutilConsoleMessage::TYPE_OUT: $which = STDOUT; break; case PhutilConsoleMessage::TYPE_ERR: $which = STDERR; break; } switch ($type) { case PhutilConsoleMessage::TYPE_TTY: if (function_exists('posix_isatty')) { $is_a_tty = posix_isatty($which); } else { $is_a_tty = null; } return $this->buildMessage(PhutilConsoleMessage::TYPE_IS_TTY, $is_a_tty); case PhutilConsoleMessage::TYPE_COLS: // TODO: This is an approximation which might not be perfectly // accurate. $width = phutil_console_get_terminal_width(); return $this->buildMessage(PhutilConsoleMessage::TYPE_COL_WIDTH, $width); } break; default: if ($this->handler) { return call_user_func($this->handler, $message); } else { throw new Exception(pht("Received unknown console message of type '%s'.", $type)); } } }
public function run() { $console = PhutilConsole::getConsole(); $uri = $this->determineConduitURI(); $this->setConduitURI($uri); $configuration_manager = $this->getConfigurationManager(); $config = $configuration_manager->readUserConfigurationFile(); $this->writeInfo(pht('CONNECT'), pht('Connecting to "%s"...', $uri)); $conduit = $this->establishConduit()->getConduit(); try { $conduit->callMethodSynchronous('conduit.ping', array()); } catch (Exception $ex) { throw new ArcanistUsageException(pht('Failed to connect to server (%s): %s', $uri, $ex->getMessage())); } $token_uri = new PhutilURI($uri); $token_uri->setPath('/conduit/token/'); // Check if this server supports the more modern token-based login. $is_token_auth = false; try { $capabilities = $conduit->callMethodSynchronous('conduit.getcapabilities', array()); $auth = idx($capabilities, 'authentication', array()); if (in_array('token', $auth)) { $token_uri->setPath('/conduit/login/'); $is_token_auth = true; } } catch (Exception $ex) { // Ignore. } echo phutil_console_format("**%s**\n", pht('LOGIN TO PHABRICATOR')); echo phutil_console_format("%s\n\n%s\n\n%s", pht('Open this page in your browser and login to ' . 'Phabricator if necessary:'), $token_uri, pht('Then paste the API Token on that page below.')); do { $token = phutil_console_prompt(pht('Paste API Token from that page:')); $token = trim($token); if (strlen($token)) { break; } } while (true); if ($is_token_auth) { if (strlen($token) != 32) { throw new ArcanistUsageException(pht('The token "%s" is not formatted correctly. API tokens should ' . 'be 32 characters long. Make sure you visited the correct URI ' . 'and copy/pasted the token correctly.', $token)); } if (strncmp($token, 'cli-', 4) !== 0) { throw new ArcanistUsageException(pht('The token "%s" is not formatted correctly. Valid API tokens ' . 'should begin "cli-" and be 32 characters long. Make sure you ' . 'visited the correct URI and copy/pasted the token correctly.', $token)); } $conduit->setConduitToken($token); try { $conduit->callMethodSynchronous('user.whoami', array()); } catch (Exception $ex) { throw new ArcanistUsageException(pht('The token "%s" is not a valid API Token. The server returned ' . 'this response when trying to use it as a token: %s', $token, $ex->getMessage())); } $config['hosts'][$uri] = array('token' => $token); } else { echo "\n"; echo pht('Downloading authentication certificate...') . "\n"; $info = $conduit->callMethodSynchronous('conduit.getcertificate', array('token' => $token, 'host' => $uri)); $user = $info['username']; echo pht("Installing certificate for '%s'...", $user) . "\n"; $config['hosts'][$uri] = array('user' => $user, 'cert' => $info['certificate']); } echo pht('Writing %s...', '~/.arcrc') . "\n"; $configuration_manager->writeUserConfigurationFile($config); if ($is_token_auth) { echo phutil_console_format("<bg:green>** %s **</bg> %s\n", pht('SUCCESS!'), pht('API Token installed.')); } else { echo phutil_console_format("<bg:green>** %s **</bg> %s\n", pht('SUCCESS!'), pht('Certificate installed.')); } return 0; }
/** * Handle resolving "arc:*" rules. */ private function resolveArcRule($rule, $name, $source) { switch ($name) { case 'verbose': $this->verbose = true; $this->log("Enabled verbose mode."); break; case 'prompt': $reason = "it is what you typed when prompted."; $this->api->setBaseCommitExplanation($reason); return phutil_console_prompt('Against which commit?'); case 'local': case 'global': case 'project': case 'args': case 'system': // Push the other source on top of the list. array_unshift($this->try, $name); $this->log("Switching to source '{$name}'."); return false; case 'yield': // Cycle this source to the end of the list. $this->try[] = array_shift($this->try); $this->log("Yielding processing of rules from '{$source}'."); return false; case 'halt': // Dump the whole stack. $this->try = array(); $this->log("Halting all rule processing."); return false; case 'skip': return null; case 'empty': case 'upstream': case 'outgoing': case 'bookmark': case 'amended': case 'this': return $this->api->resolveBaseCommitRule($rule, $source); default: $matches = null; if (preg_match('/^exec\\((.*)\\)$/', $name, $matches)) { $root = $this->api->getWorkingCopyIdentity()->getProjectRoot(); $future = new ExecFuture('%C', $matches[1]); $future->setCWD($root); list($err, $stdout) = $future->resolve(); if (!$err) { return trim($stdout); } else { return null; } } else { if (preg_match('/^nodiff\\((.*)\\)$/', $name, $matches)) { return $this->api->resolveBaseCommitRule($rule, $source); } } throw new ArcanistUsageException("Base commit rule '{$rule}' (from source '{$source}') " . "is not a recognized rule."); } }
do { $email = phutil_console_prompt(pht('Enter user email address:')); $duplicate = id(new PhabricatorUserEmail())->loadOneWhere('address = %s', $email); if ($duplicate) { echo pht("ERROR: There is already a user with that email address. " . "Each user must have a unique email address.\n"); } else { break; } } while (true); $create_email = $email; } $changed_pass = false; // This disables local echo, so the user's password is not shown as they type // it. phutil_passthru('stty -echo'); $password = phutil_console_prompt(pht('Enter a password for this user [blank to leave unchanged]:')); phutil_passthru('stty echo'); if (strlen($password)) { $changed_pass = $password; } $is_system_agent = $user->getIsSystemAgent(); $set_system_agent = phutil_console_confirm(pht('Is this user a bot?'), $default_no = !$is_system_agent); $verify_email = null; $set_verified = false; // Allow administrators to verify primary email addresses at this time in edit // scenarios. (Create will work just fine from here as we auto-verify email // on create.) if (!$is_new) { $verify_email = $user->loadPrimaryEmail(); if (!$verify_email->getIsVerified()) { $set_verified = phutil_console_confirm(pht('Should the primary email address be verified?'), $default_no = true);
private function getErrorExcuse($prompt, $history) { if ($this->getArgument('excuse')) { $prompt .= " Ignore them?"; if (!phutil_console_confirm($prompt)) { throw new ArcanistUserAbortException(); } return $this->getArgument('excuse'); } $history = $this->getRepositoryAPI()->getScratchFilePath($history); $prompt .= " Provide explanation to continue or press Enter to abort."; echo "\n\n"; echo phutil_console_wrap($prompt); $return = phutil_console_prompt("Explanation:", $history); if ($return == '') { throw new ArcanistUserAbortException(); } return $return; }
private function liberateCreateLibrary($path) { $init_path = $path . '/__phutil_library_init__.php'; if (Filesystem::pathExists($init_path)) { return; } echo pht("Creating new libphutil library in '%s'.", $path) . "\n"; do { $name = $this->getArgument('library-name'); if ($name === null) { echo pht('Choose a name for the new library.') . "\n"; $name = phutil_console_prompt(pht('What do you want to name this library?')); } else { echo pht('Using library name %s.', $name) . "\n"; } if (preg_match('/^[a-z-]+$/', $name)) { break; } else { echo phutil_console_format("%s\n", pht('Library name should contain only lowercase letters and hyphens.')); } } while (true); $template = "<?php\n\n" . "phutil_register_library('{$name}', __FILE__);\n"; echo pht("Writing '%s' to '%s'...\n", '__phutil_library_init__.php', $path); Filesystem::writeFile($init_path, $template); $this->liberateVersion2($path); }
#!/usr/bin/env php <?php require_once dirname(__FILE__) . '/../__init_script__.php'; $args = new PhutilArgumentParser($argv); $args->setTagline(pht('test console prompting')); $args->setSynopsis(<<<EOHELP **prompt.php** __options__ Test console prompting. EOHELP ); $args->parseStandardArguments(); $args->parse(array(array('name' => 'history', 'param' => 'file', 'default' => '', 'help' => pht('Use specified history __file__.')), array('name' => 'prompt', 'param' => 'text', 'default' => pht('Enter some text:'), 'help' => pht('Change the prompt text to __text__.')))); $result = phutil_console_prompt($args->getArg('prompt'), $args->getArg('history')); $console = PhutilConsole::getConsole(); $console->writeOut("%s\n", pht('Input is: %s', $result));
public final function requireCleanWorkingCopy() { $api = $this->getRepositoryAPI(); $must_commit = array(); $working_copy_desc = phutil_console_format(" Working copy: __%s__\n\n", $api->getPath()); $untracked = $api->getUntrackedChanges(); if ($this->shouldRequireCleanUntrackedFiles()) { if (!empty($untracked)) { echo "You have untracked files in this working copy.\n\n" . $working_copy_desc . " Untracked files in working copy:\n" . " " . implode("\n ", $untracked) . "\n\n"; if ($api instanceof ArcanistGitAPI) { echo phutil_console_wrap("Since you don't have '.gitignore' rules for these files and have " . "not listed them in '.git/info/exclude', you may have forgotten " . "to 'git add' them to your commit.\n"); } else { if ($api instanceof ArcanistSubversionAPI) { echo phutil_console_wrap("Since you don't have 'svn:ignore' rules for these files, you may " . "have forgotten to 'svn add' them.\n"); } else { if ($api instanceof ArcanistMercurialAPI) { echo phutil_console_wrap("Since you don't have '.hgignore' rules for these files, you " . "may have forgotten to 'hg add' them to your commit.\n"); } } } if ($this->askForAdd($untracked)) { $api->addToCommit($untracked); $must_commit += array_flip($untracked); } else { if ($this->commitMode == self::COMMIT_DISABLE) { $prompt = $this->getAskForAddPrompt($untracked); if (phutil_console_confirm($prompt)) { throw new ArcanistUsageException(pht("Add these files and then run 'arc %s' again.", $this->getWorkflowName())); } } } } } // NOTE: this is a subversion-only concept. $incomplete = $api->getIncompleteChanges(); if ($incomplete) { throw new ArcanistUsageException("You have incompletely checked out directories in this working copy. " . "Fix them before proceeding.\n\n" . $working_copy_desc . " Incomplete directories in working copy:\n" . " " . implode("\n ", $incomplete) . "\n\n" . "You can fix these paths by running 'svn update' on them."); } $conflicts = $api->getMergeConflicts(); if ($conflicts) { throw new ArcanistUsageException("You have merge conflicts in this working copy. Resolve merge " . "conflicts before proceeding.\n\n" . $working_copy_desc . " Conflicts in working copy:\n" . " " . implode("\n ", $conflicts) . "\n"); } $missing = $api->getMissingChanges(); if ($missing) { throw new ArcanistUsageException(pht("You have missing files in this working copy. Revert or formally " . "remove them (with `svn rm`) before proceeding.\n\n" . "%s" . " Missing files in working copy:\n%s\n", $working_copy_desc, " " . implode("\n ", $missing))); } $unstaged = $api->getUnstagedChanges(); if ($unstaged) { echo "You have unstaged changes in this working copy.\n\n" . $working_copy_desc . " Unstaged changes in working copy:\n" . " " . implode("\n ", $unstaged) . "\n"; if ($this->askForAdd($unstaged)) { $api->addToCommit($unstaged); $must_commit += array_flip($unstaged); } else { $permit_autostash = $this->getConfigFromAnySource('arc.autostash', false); if ($permit_autostash && $api->canStashChanges()) { echo "Stashing uncommitted changes. (You can restore them with " . "`git stash pop`.)\n"; $api->stashChanges(); $this->stashed = true; } else { throw new ArcanistUsageException('Stage and commit (or revert) them before proceeding.'); } } } $uncommitted = $api->getUncommittedChanges(); foreach ($uncommitted as $key => $path) { if (array_key_exists($path, $must_commit)) { unset($uncommitted[$key]); } } if ($uncommitted) { echo "You have uncommitted changes in this working copy.\n\n" . $working_copy_desc . " Uncommitted changes in working copy:\n" . " " . implode("\n ", $uncommitted) . "\n"; if ($this->askForAdd($uncommitted)) { $must_commit += array_flip($uncommitted); } else { throw new ArcanistUncommittedChangesException('Commit (or revert) them before proceeding.'); } } if ($must_commit) { if ($this->getShouldAmend()) { $commit = head($api->getLocalCommitInformation()); $api->amendCommit($commit['message']); } else { if ($api->supportsLocalCommits()) { $commit_message = phutil_console_prompt('Enter commit message:'); if ($commit_message == '') { $commit_message = self::AUTO_COMMIT_TITLE; } $api->doCommit($commit_message); } } } }
private function liberateCreateLibrary($path) { $init_path = $path . '/__phutil_library_init__.php'; if (Filesystem::pathExists($init_path)) { return; } echo "Creating new libphutil library in '{$path}'.\n"; echo "Choose a name for the new library.\n"; do { $name = phutil_console_prompt('What do you want to name this library?'); if (preg_match('/^[a-z]+$/', $name)) { break; } else { echo "Library name should contain only lowercase letters.\n"; } } while (true); $template = "<?php\n\n" . "phutil_register_library('{$name}', __FILE__);\n"; echo "Writing '__phutil_library_init__.php' to '{$init_path}'...\n"; Filesystem::writeFile($init_path, $template); }
public function getRelativeCommit() { if ($this->relativeCommit === null) { // Detect zero-commit or one-commit repositories. There is only one // relative-commit value that makes any sense in these repositories: the // empty tree. list($err) = $this->execManualLocal('rev-parse --verify HEAD^'); if ($err) { list($err) = $this->execManualLocal('rev-parse --verify HEAD'); if ($err) { $this->repositoryHasNoCommits = true; } $this->relativeCommit = self::GIT_MAGIC_ROOT_COMMIT; return $this->relativeCommit; } $do_write = false; $default_relative = null; list($err, $upstream) = $this->execManualLocal("rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'"); if (!$err) { $default_relative = trim($upstream); } if (!$default_relative) { $default_relative = $this->readScratchFile('default-relative-commit'); $default_relative = trim($default_relative); } if (!$default_relative) { $working_copy = $this->getWorkingCopyIdentity(); if ($working_copy) { $default_relative = $working_copy->getConfig('git.default-relative-commit'); } } if (!$default_relative) { // TODO: Remove the history lesson soon. echo phutil_console_format("<bg:green>** Select a Default Commit Range **</bg>\n\n"); echo phutil_console_wrap("You're running a command which operates on a range of revisions " . "(usually, from some revision to HEAD) but have not specified the " . "revision that should determine the start of the range.\n\n" . "Previously, arc assumed you meant 'HEAD^' when you did not specify " . "a start revision, but this behavior does not make much sense in " . "most workflows outside of Facebook's historic git-svn workflow.\n\n" . "arc no longer assumes 'HEAD^'. You must specify a relative commit " . "explicitly when you invoke a command (e.g., `arc diff HEAD^`, not " . "just `arc diff`) or select a default for this working copy.\n\n" . "In most cases, the best default is 'origin/master'. You can also " . "select 'HEAD^' to preserve the old behavior, or some other remote " . "or branch. But you almost certainly want to select " . "'origin/master'.\n\n" . "(Technically: the merge-base of the selected revision and HEAD is " . "used to determine the start of the commit range.)"); $prompt = "What default do you want to use? [origin/master]"; $default = phutil_console_prompt($prompt); if (!strlen(trim($default))) { $default = 'origin/master'; } $default_relative = $default; $do_write = true; } list($object_type) = $this->execxLocal('cat-file -t %s', $default_relative); if (trim($object_type) !== 'commit') { throw new Exception("Relative commit '{$relative}' is not the name of a commit!"); } if ($do_write) { // Don't perform this write until we've verified that the object is a // valid commit name. $this->writeScratchFile('default-relative-commit', $default_relative); } list($merge_base) = $this->execxLocal('merge-base %s HEAD', $default_relative); $this->relativeCommit = trim($merge_base); } return $this->relativeCommit; }
public function handleServerMessage(PhutilConsoleMessage $message) { $data = $message->getData(); if ($this->getArgument('excuse')) { try { phutil_console_require_tty(); } catch (PhutilConsoleStdinNotInteractiveException $ex) { $this->excuses[$data['type']] = $this->getArgument('excuse'); return null; } } $response = ''; if (isset($data['prompt'])) { $response = phutil_console_prompt($data['prompt'], idx($data, 'history')); } else { if (phutil_console_confirm($data['confirm'])) { $response = $this->getArgument('excuse'); } } if ($response == '') { throw new ArcanistUserAbortException(); } $this->excuses[$data['type']] = $response; return null; }
protected function buildBaseCommit($symbolic_commit) { if ($symbolic_commit !== null) { if ($symbolic_commit == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) { $this->setBaseCommitExplanation('you explicitly specified the empty tree.'); return $symbolic_commit; } list($err, $merge_base) = $this->execManualLocal('merge-base %s %s', $symbolic_commit, $this->getHeadCommit()); if ($err) { throw new ArcanistUsageException("Unable to find any git commit named '{$symbolic_commit}' in " . "this repository."); } if ($this->symbolicHeadCommit === null) { $this->setBaseCommitExplanation("it is the merge-base of the explicitly specified base commit " . "'{$symbolic_commit}' and HEAD."); } else { $this->setBaseCommitExplanation("it is the merge-base of the explicitly specified base commit " . "'{$symbolic_commit}' and the explicitly specified head " . "commit '{$this->symbolicHeadCommit}'."); } return trim($merge_base); } // Detect zero-commit or one-commit repositories. There is only one // relative-commit value that makes any sense in these repositories: the // empty tree. list($err) = $this->execManualLocal('rev-parse --verify HEAD^'); if ($err) { list($err) = $this->execManualLocal('rev-parse --verify HEAD'); if ($err) { $this->repositoryHasNoCommits = true; } if ($this->repositoryHasNoCommits) { $this->setBaseCommitExplanation('the repository has no commits.'); } else { $this->setBaseCommitExplanation('the repository has only one commit.'); } return self::GIT_MAGIC_ROOT_COMMIT; } if ($this->getBaseCommitArgumentRules() || $this->getConfigurationManager()->getConfigFromAnySource('base')) { $base = $this->resolveBaseCommit(); if (!$base) { throw new ArcanistUsageException("None of the rules in your 'base' configuration matched a valid " . "commit. Adjust rules or specify which commit you want to use " . "explicitly."); } return $base; } $do_write = false; $default_relative = null; $working_copy = $this->getWorkingCopyIdentity(); if ($working_copy) { $default_relative = $working_copy->getProjectConfig('git.default-relative-commit'); $this->setBaseCommitExplanation("it is the merge-base of '{$default_relative}' and HEAD, as " . "specified in 'git.default-relative-commit' in '.arcconfig'. This " . "setting overrides other settings."); } if (!$default_relative) { list($err, $upstream) = $this->execManualLocal('rev-parse --abbrev-ref --symbolic-full-name %s', '@{upstream}'); if (!$err) { $default_relative = trim($upstream); $this->setBaseCommitExplanation("it is the merge-base of '{$default_relative}' (the Git upstream " . "of the current branch) HEAD."); } } if (!$default_relative) { $default_relative = $this->readScratchFile('default-relative-commit'); $default_relative = trim($default_relative); if ($default_relative) { $this->setBaseCommitExplanation("it is the merge-base of '{$default_relative}' and HEAD, as " . "specified in '.git/arc/default-relative-commit'."); } } if (!$default_relative) { // TODO: Remove the history lesson soon. echo phutil_console_format("<bg:green>** Select a Default Commit Range **</bg>\n\n"); echo phutil_console_wrap("You're running a command which operates on a range of revisions " . "(usually, from some revision to HEAD) but have not specified the " . "revision that should determine the start of the range.\n\n" . "Previously, arc assumed you meant 'HEAD^' when you did not specify " . "a start revision, but this behavior does not make much sense in " . "most workflows outside of Facebook's historic git-svn workflow.\n\n" . "arc no longer assumes 'HEAD^'. You must specify a relative commit " . "explicitly when you invoke a command (e.g., `arc diff HEAD^`, not " . "just `arc diff`) or select a default for this working copy.\n\n" . "In most cases, the best default is 'origin/master'. You can also " . "select 'HEAD^' to preserve the old behavior, or some other remote " . "or branch. But you almost certainly want to select " . "'origin/master'.\n\n" . "(Technically: the merge-base of the selected revision and HEAD is " . "used to determine the start of the commit range.)"); $prompt = 'What default do you want to use? [origin/master]'; $default = phutil_console_prompt($prompt); if (!strlen(trim($default))) { $default = 'origin/master'; } $default_relative = $default; $do_write = true; } list($object_type) = $this->execxLocal('cat-file -t %s', $default_relative); if (trim($object_type) !== 'commit') { throw new Exception("Relative commit '{$default_relative}' is not the name of a commit!"); } if ($do_write) { // Don't perform this write until we've verified that the object is a // valid commit name. $this->writeScratchFile('default-relative-commit', $default_relative); $this->setBaseCommitExplanation("it is the merge-base of '{$default_relative}' and HEAD, as you " . "just specified."); } list($merge_base) = $this->execxLocal('merge-base %s HEAD', $default_relative); return trim($merge_base); }
/** * Detect alternate branches and prompt the user for how to handle * them. An alternate branch is a branch that forks from the landing * branch prior to the landing branch tip. * * In a situation like this: * -a--------b master * \ * w--x landingbranch * \ \-- g subbranch * \--y altbranch1 * \--z altbranch2 * * y and z are alternate branches and will get deleted by the squash, * so we need to detect them and ask the user what they want to do. * * @param string The revision id of the landing branch's root commit. * @param string The revset specifying all the commits in the landing branch. * @return void */ private function handleAlternateBranches($branch_root, $branch_range) { $repository_api = $this->getRepositoryAPI(); // Using the tree in the doccomment, the revset below resolves as follows: // 1. roots(descendants(w) - descendants(x) - (w::x)) // 2. roots({x,g,y,z} - {g} - {w,x}) // 3. roots({y,z}) // 4. {y,z} $alt_branch_revset = hgsprintf('roots(descendants(%s)-descendants(%s)-%R)', $branch_root, $this->branch, $branch_range); list($alt_branches) = $repository_api->execxLocal("log --template '{node}\n' -r %s", $alt_branch_revset); $alt_branches = phutil_split_lines($alt_branches, false); $alt_branches = array_filter($alt_branches); $alt_count = count($alt_branches); if ($alt_count > 0) { $input = phutil_console_prompt(ucfirst($this->branchType) . " '{$this->branch}' has {$alt_count} " . "{$this->branchType}(s) forking off of it that would be deleted " . "during a squash. Would you like to keep a non-squashed copy, rebase " . "them on top of '{$this->branch}', or abort and deal with them " . "yourself? (k)eep, (r)ebase, (a)bort:"); if ($input == 'k' || $input == 'keep') { $this->keepBranch = true; } else { if ($input == 'r' || $input == 'rebase') { foreach ($alt_branches as $alt_branch) { $repository_api->execxLocal('rebase --keep --keepbranches -d %s -s %s', $this->branch, $alt_branch); } } else { if ($input == 'a' || $input == 'abort') { $branch_string = implode("\n", $alt_branches); echo "\nRemove the {$this->branchType}s starting at these revisions " . "and run arc land again:\n{$branch_string}\n\n"; throw new ArcanistUserAbortException(); } else { throw new ArcanistUsageException("Invalid choice. Aborting arc land."); } } } } }