mirror of
https://we.phorge.it/source/arcanist.git
synced 2025-01-19 19:21:09 +01:00
(stable) Merge 2015 Week 44
This commit is contained in:
commit
dbd45635ec
9 changed files with 1135 additions and 29 deletions
|
@ -112,6 +112,8 @@ phutil_register_library_map(array(
|
|||
'ArcanistGeneratedLinterTestCase' => 'lint/linter/__tests__/ArcanistGeneratedLinterTestCase.php',
|
||||
'ArcanistGetConfigWorkflow' => 'workflow/ArcanistGetConfigWorkflow.php',
|
||||
'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php',
|
||||
'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php',
|
||||
'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php',
|
||||
'ArcanistGlobalVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistGlobalVariableXHPASTLinterRule.php',
|
||||
'ArcanistGoLintLinter' => 'lint/linter/ArcanistGoLintLinter.php',
|
||||
'ArcanistGoLintLinterTestCase' => 'lint/linter/__tests__/ArcanistGoLintLinterTestCase.php',
|
||||
|
@ -144,6 +146,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistJscsLinterTestCase' => 'lint/linter/__tests__/ArcanistJscsLinterTestCase.php',
|
||||
'ArcanistKeywordCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php',
|
||||
'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLambdaFuncFunctionXHPASTLinterRule.php',
|
||||
'ArcanistLandEngine' => 'land/ArcanistLandEngine.php',
|
||||
'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php',
|
||||
'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLanguageConstructParenthesesXHPASTLinterRule.php',
|
||||
'ArcanistLesscLinter' => 'lint/linter/ArcanistLesscLinter.php',
|
||||
|
@ -398,6 +401,8 @@ phutil_register_library_map(array(
|
|||
'ArcanistGeneratedLinterTestCase' => 'ArcanistLinterTestCase',
|
||||
'ArcanistGetConfigWorkflow' => 'ArcanistWorkflow',
|
||||
'ArcanistGitAPI' => 'ArcanistRepositoryAPI',
|
||||
'ArcanistGitLandEngine' => 'ArcanistLandEngine',
|
||||
'ArcanistGitUpstreamPath' => 'Phobject',
|
||||
'ArcanistGlobalVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||
'ArcanistGoLintLinter' => 'ArcanistExternalLinter',
|
||||
'ArcanistGoLintLinterTestCase' => 'ArcanistExternalLinterTestCase',
|
||||
|
@ -430,6 +435,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase',
|
||||
'ArcanistKeywordCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||
'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||
'ArcanistLandEngine' => 'Phobject',
|
||||
'ArcanistLandWorkflow' => 'ArcanistWorkflow',
|
||||
'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||
'ArcanistLesscLinter' => 'ArcanistExternalLinter',
|
||||
|
|
|
@ -78,6 +78,11 @@ final class ArcanistUSEnglishTranslation extends PhutilTranslation {
|
|||
'Ignore the changes to this submodule and continue?',
|
||||
'Ignore the changes to these submodules and continue?',
|
||||
),
|
||||
|
||||
'These %s commit(s) will be landed:' => array(
|
||||
'This commit will be landed:',
|
||||
'These commits will be landed:',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
532
src/land/ArcanistGitLandEngine.php
Normal file
532
src/land/ArcanistGitLandEngine.php
Normal file
|
@ -0,0 +1,532 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistGitLandEngine
|
||||
extends ArcanistLandEngine {
|
||||
|
||||
private $localRef;
|
||||
private $localCommit;
|
||||
private $sourceCommit;
|
||||
private $mergedRef;
|
||||
private $restoreWhenDestroyed;
|
||||
|
||||
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->writeInfo(
|
||||
pht('HOLD'),
|
||||
pht('Holding change locally, it has not been pushed.'));
|
||||
} 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) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Branch "%s" does not exist in 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();
|
||||
|
||||
$this->writeInfo(
|
||||
pht('FETCH'),
|
||||
pht('Fetching %s...', $ref));
|
||||
|
||||
$api->execxLocal(
|
||||
'fetch -- %s %s',
|
||||
$this->getTargetRemote(),
|
||||
$this->getTargetOnto());
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
list($changes) = $api->execxLocal('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();
|
||||
|
||||
$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 "%s" again.',
|
||||
'arc land'));
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
$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;
|
||||
}
|
||||
|
||||
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 ($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 --');
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if ($this->getSourceRef() == $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?
|
||||
|
||||
$recovery_command = csprintf(
|
||||
'git checkout -b %R %R',
|
||||
$this->getSourceRef(),
|
||||
$this->sourceCommit);
|
||||
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht('Cleaning up branch "%s"...', $this->getSourceRef()));
|
||||
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht('(Use `%s` if you want it back.)', $recovery_command));
|
||||
|
||||
$api->execxLocal('branch -D -- %s', $this->getSourceRef());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
161
src/land/ArcanistLandEngine.php
Normal file
161
src/land/ArcanistLandEngine.php
Normal file
|
@ -0,0 +1,161 @@
|
|||
<?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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1333,4 +1333,75 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
|
|||
$this->resolvedHeadCommit = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow the chain of tracking branches upstream until we reach a remote
|
||||
* or cycle locally.
|
||||
*
|
||||
* @param string Ref to start from.
|
||||
* @return list<wild> Path to an upstream.
|
||||
*/
|
||||
public function getPathToUpstream($start) {
|
||||
$cursor = $start;
|
||||
$path = new ArcanistGitUpstreamPath();
|
||||
while (true) {
|
||||
list($err, $upstream) = $this->execManualLocal(
|
||||
'rev-parse --symbolic-full-name %s@{upstream}',
|
||||
$cursor);
|
||||
|
||||
if ($err) {
|
||||
// We ended up somewhere with no tracking branch, so we're done.
|
||||
break;
|
||||
}
|
||||
|
||||
$upstream = trim($upstream);
|
||||
|
||||
if (preg_match('(^refs/heads/)', $upstream)) {
|
||||
$upstream = preg_replace('(^refs/heads/)', '', $upstream);
|
||||
|
||||
$is_cycle = $path->getUpstream($upstream);
|
||||
|
||||
$path->addUpstream(
|
||||
$cursor,
|
||||
array(
|
||||
'type' => ArcanistGitUpstreamPath::TYPE_LOCAL,
|
||||
'name' => $upstream,
|
||||
'cycle' => $is_cycle,
|
||||
));
|
||||
|
||||
if ($is_cycle) {
|
||||
// We ran into a local cycle, so we're done.
|
||||
break;
|
||||
}
|
||||
|
||||
// We found another local branch, so follow that one upriver.
|
||||
$cursor = $upstream;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('(^refs/remotes/)', $upstream)) {
|
||||
$upstream = preg_replace('(^refs/remotes/)', '', $upstream);
|
||||
list($remote, $branch) = explode('/', $upstream, 2);
|
||||
|
||||
$path->addUpstream(
|
||||
$cursor,
|
||||
array(
|
||||
'type' => ArcanistGitUpstreamPath::TYPE_REMOTE,
|
||||
'name' => $branch,
|
||||
'remote' => $remote,
|
||||
));
|
||||
|
||||
// We found a remote, so we're done.
|
||||
break;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Got unrecognized upstream format ("%s") from Git, expected '.
|
||||
'"refs/heads/..." or "refs/remotes/...".',
|
||||
$upstream));
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
90
src/repository/api/ArcanistGitUpstreamPath.php
Normal file
90
src/repository/api/ArcanistGitUpstreamPath.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistGitUpstreamPath extends Phobject {
|
||||
|
||||
private $path = array();
|
||||
|
||||
const TYPE_LOCAL = 'local';
|
||||
const TYPE_REMOTE = 'remote';
|
||||
|
||||
|
||||
public function addUpstream($key, array $spec) {
|
||||
$this->path[$key] = $spec;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUpstream($key) {
|
||||
unset($this->path[$key]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpstream($key) {
|
||||
return idx($this->path, $key);
|
||||
}
|
||||
|
||||
public function getLength() {
|
||||
return count($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if this path eventually connects to a remote.
|
||||
*
|
||||
* @return bool True if the path connects to a remote.
|
||||
*/
|
||||
public function isConnectedToRemote() {
|
||||
$last = last($this->path);
|
||||
|
||||
if (!$last) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($last['type'] == self::TYPE_REMOTE);
|
||||
}
|
||||
|
||||
public function getLocalBranches() {
|
||||
return array_keys($this->path);
|
||||
}
|
||||
|
||||
public function getRemoteBranchName() {
|
||||
if (!$this->isConnectedToRemote()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return idx(last($this->path), 'name');
|
||||
}
|
||||
|
||||
public function getRemoteRemoteName() {
|
||||
if (!$this->isConnectedToRemote()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return idx(last($this->path), 'remote');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If this path contains a cycle, return a description of it.
|
||||
*
|
||||
* @return list<string>|null Cycle, if the path contains one.
|
||||
*/
|
||||
public function getCycle() {
|
||||
$last = last($this->path);
|
||||
if (!$last) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (empty($last['cycle'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = array();
|
||||
foreach ($this->path as $key => $item) {
|
||||
$parts[] = $key;
|
||||
}
|
||||
$parts[] = $item['name'];
|
||||
$parts[] = pht('...');
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
abstract class ArcanistUnitTestEngine extends Phobject {
|
||||
|
||||
private $workingCopy;
|
||||
private $paths;
|
||||
private $paths = array();
|
||||
private $arguments = array();
|
||||
private $enableAsyncTests;
|
||||
private $enableCoverage;
|
||||
|
|
|
@ -38,7 +38,7 @@ final class ArcanistLandWorkflow extends ArcanistWorkflow {
|
|||
|
||||
public function getCommandSynopses() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
**land** [__options__] [__branch__] [--onto __master__]
|
||||
**land** [__options__] [__ref__]
|
||||
EOTEXT
|
||||
);
|
||||
}
|
||||
|
@ -47,18 +47,71 @@ EOTEXT
|
|||
return phutil_console_format(<<<EOTEXT
|
||||
Supports: git, hg
|
||||
|
||||
Land an accepted change (currently sitting in local feature branch
|
||||
__branch__) onto __master__ and push it to the remote. Then, delete
|
||||
the feature branch. If you omit __branch__, the current branch will
|
||||
be used.
|
||||
Publish an accepted revision after review. This command is the last
|
||||
step in the standard Differential pre-publish code review workflow.
|
||||
|
||||
In mutable repositories, this will perform a --squash merge (the
|
||||
entire branch will be represented by one commit on __master__). In
|
||||
immutable repositories (or when --merge is provided), it will perform
|
||||
a --no-ff merge (the branch will always be merged into __master__ with
|
||||
a merge commit).
|
||||
This command merges and pushes changes associated with an accepted
|
||||
revision that are currently sitting in __ref__, which is usually the
|
||||
name of a local branch. Without __ref__, the current working copy
|
||||
state will be used.
|
||||
|
||||
Under Git: branches, tags, and arbitrary commits (detached HEADs)
|
||||
may be landed.
|
||||
|
||||
Under Mercurial: branches and bookmarks may be landed, but only
|
||||
onto a target of the same type. See T3855.
|
||||
|
||||
The workflow selects a target branch to land onto and a remote where
|
||||
the change will be pushed to.
|
||||
|
||||
A target branch is selected by examining these sources in order:
|
||||
|
||||
- the **--onto** flag;
|
||||
- the upstream of the current branch, recursively (Git only);
|
||||
- the __arc.land.onto.default__ configuration setting;
|
||||
- or by falling back to a standard default:
|
||||
- "master" in Git;
|
||||
- "default" in Mercurial.
|
||||
|
||||
A remote is selected by examining these sources in order:
|
||||
|
||||
- the **--remote** flag;
|
||||
- the upstream of the current branch, recursively (Git only);
|
||||
- or by falling back to a standard default:
|
||||
- "origin" in Git;
|
||||
- the default remote in Mercurial.
|
||||
|
||||
After selecting a target branch and a remote, the commits which will
|
||||
be landed are printed.
|
||||
|
||||
With **--preview**, execution stops here, before the change is
|
||||
merged.
|
||||
|
||||
The change is merged into the target branch, following these rules:
|
||||
|
||||
In mutable repositories or with **--squash**, this will perform a
|
||||
squash merge (the entire branch will be represented as one commit on
|
||||
the target branch).
|
||||
|
||||
In immutable repositories or with **--merge**, this will perform a
|
||||
strict merge (a merge commit will always be created, and local
|
||||
commits will be preserved).
|
||||
|
||||
The resulting commit will be given an up-to-date commit message
|
||||
describing the final state of the revision in Differential.
|
||||
|
||||
With **--hold**, execution stops here, before the change is pushed.
|
||||
|
||||
The change is pushed into the remote.
|
||||
|
||||
Consulting mystical sources of power, the workflow makes a guess
|
||||
about what state you wanted to end up in after the process finishes
|
||||
and the working copy is put into that state.
|
||||
|
||||
The branch which was landed is deleted, unless the **--keep-branch**
|
||||
flag was passed or the landing branch is the same as the target
|
||||
branch.
|
||||
|
||||
Under hg, bookmarks can be landed the same way as branches.
|
||||
EOTEXT
|
||||
);
|
||||
}
|
||||
|
@ -196,6 +249,55 @@ EOTEXT
|
|||
|
||||
public function run() {
|
||||
$this->readArguments();
|
||||
|
||||
$engine = null;
|
||||
if ($this->isGit && !$this->isGitSvn) {
|
||||
$engine = new ArcanistGitLandEngine();
|
||||
}
|
||||
|
||||
if ($engine) {
|
||||
$this->readEngineArguments();
|
||||
|
||||
$obsolete = array(
|
||||
'delete-remote',
|
||||
'update-with-merge',
|
||||
'update-with-rebase',
|
||||
);
|
||||
|
||||
foreach ($obsolete as $flag) {
|
||||
if ($this->getArgument($flag)) {
|
||||
throw new ArcanistUsageException(
|
||||
pht(
|
||||
'Flag "%s" is no longer supported under Git.',
|
||||
'--'.$flag));
|
||||
}
|
||||
}
|
||||
|
||||
$this->requireCleanWorkingCopy();
|
||||
|
||||
$should_hold = $this->getArgument('hold');
|
||||
|
||||
$engine
|
||||
->setWorkflow($this)
|
||||
->setRepositoryAPI($this->getRepositoryAPI())
|
||||
->setSourceRef($this->branch)
|
||||
->setTargetRemote($this->remote)
|
||||
->setTargetOnto($this->onto)
|
||||
->setShouldHold($should_hold)
|
||||
->setShouldKeep($this->keepBranch)
|
||||
->setShouldSquash($this->useSquash)
|
||||
->setShouldPreview($this->preview)
|
||||
->setBuildMessageCallback(array($this, 'buildEngineMessage'));
|
||||
|
||||
$engine->execute();
|
||||
|
||||
if (!$should_hold && !$this->preview) {
|
||||
$this->didPush();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
|
@ -256,6 +358,123 @@ EOTEXT
|
|||
return null;
|
||||
}
|
||||
|
||||
private function readEngineArguments() {
|
||||
// NOTE: This is hard-coded for Git right now.
|
||||
// TODO: Clean this up and move it into LandEngines.
|
||||
|
||||
$onto = $this->getEngineOnto();
|
||||
$remote = $this->getEngineRemote();
|
||||
|
||||
// This just overwrites work we did earlier, but it has to be up in this
|
||||
// class for now because other parts of the workflow still depend on it.
|
||||
$this->onto = $onto;
|
||||
$this->remote = $remote;
|
||||
$this->ontoRemoteBranch = $this->remote.'/'.$onto;
|
||||
}
|
||||
|
||||
private function getEngineOnto() {
|
||||
$onto = $this->getArgument('onto');
|
||||
if ($onto !== null) {
|
||||
$this->writeInfo(
|
||||
pht('TARGET'),
|
||||
pht(
|
||||
'Landing onto "%s", selected by the --onto flag.',
|
||||
$onto));
|
||||
return $onto;
|
||||
}
|
||||
|
||||
$api = $this->getRepositoryAPI();
|
||||
$path = $api->getPathToUpstream($this->branch);
|
||||
|
||||
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.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$config_key = 'arc.land.onto.default';
|
||||
$onto = $this->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() {
|
||||
$remote = $this->getArgument('remote');
|
||||
if ($remote !== null) {
|
||||
$this->writeInfo(
|
||||
pht('REMOTE'),
|
||||
pht(
|
||||
'Using remote "%s", selected by the --remote flag.',
|
||||
$remote));
|
||||
return $remote;
|
||||
}
|
||||
|
||||
$api = $this->getRepositoryAPI();
|
||||
$path = $api->getPathToUpstream($this->branch);
|
||||
|
||||
$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 = 'origin';
|
||||
$this->writeInfo(
|
||||
pht('REMOTE'),
|
||||
pht(
|
||||
'Using remote "%s", the default remote under git.',
|
||||
$remote));
|
||||
return $remote;
|
||||
}
|
||||
|
||||
|
||||
private function readArguments() {
|
||||
$repository_api = $this->getRepositoryAPI();
|
||||
$this->isGit = $repository_api instanceof ArcanistGitAPI;
|
||||
|
@ -273,9 +492,12 @@ EOTEXT
|
|||
$branch = $this->getArgument('branch');
|
||||
if (empty($branch)) {
|
||||
$branch = $this->getBranchOrBookmark();
|
||||
|
||||
if ($branch) {
|
||||
$this->branchType = $this->getBranchType($branch);
|
||||
|
||||
// TODO: This message is misleading when landing a detached head or
|
||||
// a tag in Git.
|
||||
|
||||
echo pht("Landing current %s '%s'.", $this->branchType, $branch), "\n";
|
||||
$branch = array($branch);
|
||||
}
|
||||
|
@ -1032,9 +1254,7 @@ EOTEXT
|
|||
// We dispatch this event so we can run checks on the merged revision,
|
||||
// right before it gets pushed out. It's easier to do this in arc land
|
||||
// than to try to hook into git/hg.
|
||||
$this->dispatchEvent(
|
||||
ArcanistEventType::TYPE_LAND_WILLPUSHREVISION,
|
||||
array());
|
||||
$this->didCommitMerge();
|
||||
} catch (Exception $ex) {
|
||||
$this->executeCleanupAfterFailedPush();
|
||||
throw $ex;
|
||||
|
@ -1085,16 +1305,7 @@ EOTEXT
|
|||
$cmd));
|
||||
}
|
||||
|
||||
$this->askForRepositoryUpdate();
|
||||
|
||||
$mark_workflow = $this->buildChildWorkflow(
|
||||
'close-revision',
|
||||
array(
|
||||
'--finalize',
|
||||
'--quiet',
|
||||
$this->revision['id'],
|
||||
));
|
||||
$mark_workflow->run();
|
||||
$this->didPush();
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
@ -1193,6 +1404,11 @@ EOTEXT
|
|||
$repository_api = $this->getRepositoryAPI();
|
||||
if ($this->isGit) {
|
||||
$branch = $repository_api->getBranchName();
|
||||
|
||||
// If we don't have a branch name, just use whatever's at HEAD.
|
||||
if (!strlen($branch) && !$this->isGitSvn) {
|
||||
$branch = $repository_api->getWorkingCopyRevision();
|
||||
}
|
||||
} else if ($this->isHg) {
|
||||
$branch = $repository_api->getActiveBookmark();
|
||||
if (!$branch) {
|
||||
|
@ -1317,4 +1533,29 @@ EOTEXT
|
|||
}
|
||||
}
|
||||
|
||||
public function buildEngineMessage(ArcanistLandEngine $engine) {
|
||||
// TODO: This is oh-so-gross.
|
||||
$this->findRevision();
|
||||
$engine->setCommitMessageFile($this->messageFile);
|
||||
}
|
||||
|
||||
public function didCommitMerge() {
|
||||
$this->dispatchEvent(
|
||||
ArcanistEventType::TYPE_LAND_WILLPUSHREVISION,
|
||||
array());
|
||||
}
|
||||
|
||||
public function didPush() {
|
||||
$this->askForRepositoryUpdate();
|
||||
|
||||
$mark_workflow = $this->buildChildWorkflow(
|
||||
'close-revision',
|
||||
array(
|
||||
'--finalize',
|
||||
'--quiet',
|
||||
$this->revision['id'],
|
||||
));
|
||||
$mark_workflow->run();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1361,7 +1361,7 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
fwrite(STDERR, $msg);
|
||||
}
|
||||
|
||||
final protected function writeInfo($title, $message) {
|
||||
final public function writeInfo($title, $message) {
|
||||
$this->writeStatusMessage(
|
||||
phutil_console_format(
|
||||
"<bg:blue>** %s **</bg> %s\n",
|
||||
|
@ -1369,7 +1369,7 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
$message));
|
||||
}
|
||||
|
||||
final protected function writeWarn($title, $message) {
|
||||
final public function writeWarn($title, $message) {
|
||||
$this->writeStatusMessage(
|
||||
phutil_console_format(
|
||||
"<bg:yellow>** %s **</bg> %s\n",
|
||||
|
@ -1377,7 +1377,7 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
$message));
|
||||
}
|
||||
|
||||
final protected function writeOkay($title, $message) {
|
||||
final public function writeOkay($title, $message) {
|
||||
$this->writeStatusMessage(
|
||||
phutil_console_format(
|
||||
"<bg:green>** %s **</bg> %s\n",
|
||||
|
|
Loading…
Reference in a new issue