mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-29 10:12:41 +01:00
Break arc land run() into smaller functions
Summary: Refactor the arc land code into several functions so it's easier to maintain. This is in preparation for adding hg support to arc land in my next commit. Without this refactor, adding hg support makes the run() function too big. Test Plan: Set up a git repo and clone with a branch scenario. Example: https://secure.phabricator.com/P614 Ran and verified: arc land arc land --keep-branch arc land --merge arc land --merge --keep-branch arc land --hold arc land --revision <another phabricator rev> Reviewers: epriestley Reviewed By: epriestley CC: dschleimer, sid0, aran, Korvin Differential Revision: https://secure.phabricator.com/D4056
This commit is contained in:
parent
b549f565c9
commit
3602d2aa15
1 changed files with 222 additions and 148 deletions
|
@ -6,6 +6,18 @@
|
||||||
* @group workflow
|
* @group workflow
|
||||||
*/
|
*/
|
||||||
final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
|
final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
|
||||||
|
private $isGit;
|
||||||
|
|
||||||
|
private $oldBranch;
|
||||||
|
private $branch;
|
||||||
|
private $onto;
|
||||||
|
private $ontoRemoteBranch;
|
||||||
|
private $remote;
|
||||||
|
private $useSquash;
|
||||||
|
private $keepBranch;
|
||||||
|
|
||||||
|
private $revision;
|
||||||
|
private $message;
|
||||||
|
|
||||||
public function getWorkflowName() {
|
public function getWorkflowName() {
|
||||||
return 'land';
|
return 'land';
|
||||||
|
@ -103,14 +115,52 @@ EOTEXT
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run() {
|
public function run() {
|
||||||
|
$this->readArguments();
|
||||||
|
$this->validate();
|
||||||
|
$this->findRevision();
|
||||||
|
$this->pullFromRemote();
|
||||||
|
|
||||||
|
if ($this->useSquash) {
|
||||||
|
$this->rebase();
|
||||||
|
$this->squash();
|
||||||
|
} else {
|
||||||
|
$this->merge();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->push();
|
||||||
|
if (!$this->keepBranch) {
|
||||||
|
$this->cleanupBranch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were on some branch A and the user ran "arc land B",
|
||||||
|
// switch back to A.
|
||||||
|
if ($this->oldBranch != $this->branch && $this->oldBranch != $this->onto) {
|
||||||
$repository_api = $this->getRepositoryAPI();
|
$repository_api = $this->getRepositoryAPI();
|
||||||
if (!($repository_api instanceof ArcanistGitAPI)) {
|
$repository_api->execxLocal(
|
||||||
|
'checkout %s',
|
||||||
|
$this->oldBranch);
|
||||||
|
echo phutil_console_format(
|
||||||
|
"Switched back to branch **%s**.\n",
|
||||||
|
$this->oldBranch);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Done.\n";
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readArguments() {
|
||||||
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
$this->isGit = $repository_api instanceof ArcanistGitAPI;
|
||||||
|
|
||||||
|
if (!$this->isGit) {
|
||||||
throw new ArcanistUsageException("'arc land' only supports git.");
|
throw new ArcanistUsageException("'arc land' only supports git.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$branch = $this->getArgument('branch');
|
$branch = $this->getArgument('branch');
|
||||||
if (empty($branch)) {
|
if (empty($branch)) {
|
||||||
$branch = $repository_api->getBranchName();
|
$branch = $repository_api->getBranchName();
|
||||||
|
|
||||||
if ($branch) {
|
if ($branch) {
|
||||||
echo "Landing current branch '{$branch}'.\n";
|
echo "Landing current branch '{$branch}'.\n";
|
||||||
$branch = array($branch);
|
$branch = array($branch);
|
||||||
|
@ -121,93 +171,60 @@ EOTEXT
|
||||||
throw new ArcanistUsageException(
|
throw new ArcanistUsageException(
|
||||||
"Specify exactly one branch to land changes from.");
|
"Specify exactly one branch to land changes from.");
|
||||||
}
|
}
|
||||||
$branch = head($branch);
|
$this->branch = head($branch);
|
||||||
|
|
||||||
$onto_default = nonempty(
|
$onto_default = nonempty(
|
||||||
$this->getWorkingCopy()->getConfigFromAnySource('arc.land.onto.default'),
|
$this->getWorkingCopy()->getConfigFromAnySource('arc.land.onto.default'),
|
||||||
'master');
|
'master');
|
||||||
|
|
||||||
$remote = $this->getArgument('remote', 'origin');
|
$this->remote = $this->getArgument('remote', 'origin');
|
||||||
$onto = $this->getArgument('onto', $onto_default);
|
$this->onto = $this->getArgument('onto', $onto_default);
|
||||||
|
$this->keepBranch = $this->getArgument('keep-branch');
|
||||||
|
|
||||||
$is_immutable = $this->isHistoryImmutable();
|
if ($this->getArgument('merge')) {
|
||||||
|
$this->useSquash = false;
|
||||||
|
} else if ($this->getArgument('squash')) {
|
||||||
|
$this->useSquash = true;
|
||||||
|
} else {
|
||||||
|
$this->useSquash = !$this->isHistoryImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
if ($onto == $branch) {
|
$this->ontoRemoteBranch = $this->remote.'/'.$this->onto;
|
||||||
|
|
||||||
|
$this->oldBranch = $repository_api->getBranchName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validate() {
|
||||||
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
|
if ($this->onto == $this->branch) {
|
||||||
$message =
|
$message =
|
||||||
"You can not land a branch onto itself -- you are trying to land ".
|
"You can not land a branch onto itself -- you are trying to land ".
|
||||||
"'{$branch}' onto '{$onto}'. For more information on how to push ".
|
"'{$this->branch}' onto '{$this->onto}'. For more information on ".
|
||||||
"changes, see 'Pushing and Closing Revisions' in ".
|
"how to push changes, see 'Pushing and Closing Revisions' in ".
|
||||||
"'Arcanist User Guide: arc diff' in the documentation.";
|
"'Arcanist User Guide: arc diff' in the documentation.";
|
||||||
if (!$is_immutable) {
|
if (!$this->isHistoryImmutable()) {
|
||||||
$message .= " You may be able to 'arc amend' instead.";
|
$message .= " You may be able to 'arc amend' instead.";
|
||||||
}
|
}
|
||||||
throw new ArcanistUsageException($message);
|
throw new ArcanistUsageException($message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->getArgument('merge')) {
|
|
||||||
$use_squash = false;
|
|
||||||
} else if ($this->getArgument('squash')) {
|
|
||||||
$use_squash = true;
|
|
||||||
} else {
|
|
||||||
$use_squash = !$is_immutable;
|
|
||||||
}
|
|
||||||
|
|
||||||
list($err) = $repository_api->execManualLocal(
|
list($err) = $repository_api->execManualLocal(
|
||||||
'rev-parse --verify %s',
|
'rev-parse --verify %s',
|
||||||
$branch);
|
$this->branch);
|
||||||
|
|
||||||
if ($err) {
|
if ($err) {
|
||||||
throw new ArcanistUsageException("Branch '{$branch}' does not exist.");
|
throw new ArcanistUsageException(
|
||||||
|
"Branch '{$this->branch}' does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->requireCleanWorkingCopy();
|
$this->requireCleanWorkingCopy();
|
||||||
$repository_api->parseRelativeLocalCommit(array($remote.'/'.$onto));
|
|
||||||
|
|
||||||
$old_branch = $repository_api->getBranchName();
|
|
||||||
|
|
||||||
$repository_api->execxLocal('checkout %s', $onto);
|
|
||||||
|
|
||||||
echo phutil_console_format(
|
|
||||||
"Switched to branch **%s**. Updating branch...\n",
|
|
||||||
$onto);
|
|
||||||
|
|
||||||
$repository_api->execxLocal('pull --ff-only');
|
|
||||||
|
|
||||||
list($out) = $repository_api->execxLocal(
|
|
||||||
'log %s/%s..%s',
|
|
||||||
$remote,
|
|
||||||
$onto,
|
|
||||||
$onto);
|
|
||||||
if (strlen(trim($out))) {
|
|
||||||
throw new ArcanistUsageException(
|
|
||||||
"Local branch '{$onto}' is ahead of '{$remote}/{$onto}', so landing ".
|
|
||||||
"a feature branch would push additional changes. Push or reset the ".
|
|
||||||
"changes in '{$onto}' before running 'arc land'.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$repository_api->execxLocal(
|
private function findRevision() {
|
||||||
'checkout %s',
|
$repository_api = $this->getRepositoryAPI();
|
||||||
$branch);
|
|
||||||
|
|
||||||
echo phutil_console_format(
|
$repository_api->parseRelativeLocalCommit(array($this->ontoRemoteBranch));
|
||||||
"Switched to branch **%s**. Identifying and merging...\n",
|
|
||||||
$branch);
|
|
||||||
|
|
||||||
if ($use_squash) {
|
|
||||||
chdir($repository_api->getPath());
|
|
||||||
$err = phutil_passthru('git rebase %s', $onto);
|
|
||||||
if ($err) {
|
|
||||||
throw new ArcanistUsageException(
|
|
||||||
"'git rebase {$onto}' failed. You can abort with 'git rebase ".
|
|
||||||
"--abort', or resolve conflicts and use 'git rebase --continue' to ".
|
|
||||||
"continue forward. After resolving the rebase, run 'arc land' ".
|
|
||||||
"again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we've rebased, the merge-base of origin/master and HEAD may
|
|
||||||
// be different. Reparse the relative commit.
|
|
||||||
$repository_api->parseRelativeLocalCommit(array($remote.'/'.$onto));
|
|
||||||
}
|
|
||||||
|
|
||||||
$revision_id = $this->getArgument('revision');
|
$revision_id = $this->getArgument('revision');
|
||||||
if ($revision_id) {
|
if ($revision_id) {
|
||||||
|
@ -230,65 +247,134 @@ EOTEXT
|
||||||
|
|
||||||
if (!count($revisions)) {
|
if (!count($revisions)) {
|
||||||
throw new ArcanistUsageException(
|
throw new ArcanistUsageException(
|
||||||
"arc can not identify which revision exists on branch '{$branch}'. ".
|
"arc can not identify which revision exists on branch ".
|
||||||
"Update the revision with recent changes to synchronize the branch ".
|
"'{$this->branch}'. Update the revision with recent changes ".
|
||||||
"name and hashes, or use 'arc amend' to amend the commit message at ".
|
"to synchronize the branch name and hashes, or use 'arc amend' ".
|
||||||
"HEAD, or use '--revision <id>' to select a revision explicitly.");
|
"to amend the commit message at HEAD, or use '--revision <id>' ".
|
||||||
|
"to select a revision explicitly.");
|
||||||
} else if (count($revisions) > 1) {
|
} else if (count($revisions) > 1) {
|
||||||
$message =
|
$message =
|
||||||
"There are multiple revisions on feature branch '{$branch}' which are ".
|
"There are multiple revisions on feature branch '{$this->branch}' ".
|
||||||
"not present on '{$onto}':\n\n".
|
"which are not present on '{$onto}':\n\n".
|
||||||
$this->renderRevisionList($revisions)."\n".
|
$this->renderRevisionList($revisions)."\n".
|
||||||
"Separate these revisions onto different branches, or use ".
|
"Separate these revisions onto different branches, or use ".
|
||||||
"'--revision <id>' to select one.";
|
"'--revision <id>' to select one.";
|
||||||
throw new ArcanistUsageException($message);
|
throw new ArcanistUsageException($message);
|
||||||
}
|
}
|
||||||
|
|
||||||
$revision = head($revisions);
|
$this->revision = head($revisions);
|
||||||
$rev_id = $revision['id'];
|
|
||||||
$rev_title = $revision['title'];
|
|
||||||
|
|
||||||
if ($revision['status'] != ArcanistDifferentialRevisionStatus::ACCEPTED) {
|
$rev_status = $this->revision['status'];
|
||||||
|
$rev_id = $this->revision['id'];
|
||||||
|
$rev_title = $this->revision['title'];
|
||||||
|
|
||||||
|
if ($rev_status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
|
||||||
$ok = phutil_console_confirm(
|
$ok = phutil_console_confirm(
|
||||||
"Revision 'D{$rev_id}: {$rev_title}' has not been accepted. Continue ".
|
"Revision 'D{$rev_id}: {$rev_title}' has not been ".
|
||||||
"anyway?");
|
"accepted. Continue anyway?");
|
||||||
if (!$ok) {
|
if (!$ok) {
|
||||||
throw new ArcanistUserAbortException();
|
throw new ArcanistUserAbortException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Landing revision 'D{$rev_id}: {$rev_title}'...\n";
|
$this->message = $this->getConduit()->callMethodSynchronous(
|
||||||
|
|
||||||
$message = $this->getConduit()->callMethodSynchronous(
|
|
||||||
'differential.getcommitmessage',
|
'differential.getcommitmessage',
|
||||||
array(
|
array(
|
||||||
'revision_id' => $revision['id'],
|
'revision_id' => $rev_id,
|
||||||
));
|
));
|
||||||
|
|
||||||
$repository_api->execxLocal('checkout %s', $onto);
|
echo "Landing revision 'D{$rev_id}: ".
|
||||||
|
"{$rev_title}'...\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pullFromRemote() {
|
||||||
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
$repository_api->execxLocal('checkout %s', $this->onto);
|
||||||
|
|
||||||
|
echo phutil_console_format(
|
||||||
|
"Switched to branch **%s**. Updating branch...\n",
|
||||||
|
$this->onto);
|
||||||
|
|
||||||
|
$repository_api->execxLocal('pull --ff-only');
|
||||||
|
|
||||||
|
list($out) = $repository_api->execxLocal(
|
||||||
|
'log %s/%s..%s',
|
||||||
|
$this->remote,
|
||||||
|
$this->onto,
|
||||||
|
$this->onto);
|
||||||
|
if (strlen(trim($out))) {
|
||||||
|
throw new ArcanistUsageException(
|
||||||
|
"Local branch '{$this->onto}' is ahead of remote branch ".
|
||||||
|
"'{$this->ontoRemoteBranch}', so landing a feature branch ".
|
||||||
|
"would push additional changes. Push or reset the changes ".
|
||||||
|
"in '{$this->onto}' before running 'arc land'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function rebase() {
|
||||||
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
$repository_api->execxLocal(
|
||||||
|
'checkout %s',
|
||||||
|
$this->branch);
|
||||||
|
|
||||||
|
echo phutil_console_format(
|
||||||
|
"Switched to branch **%s**. Identifying and merging...\n",
|
||||||
|
$this->branch);
|
||||||
|
|
||||||
|
if ($this->useSquash) {
|
||||||
|
chdir($repository_api->getPath());
|
||||||
|
$err = phutil_passthru('git rebase %s', $this->onto);
|
||||||
|
|
||||||
|
if ($err) {
|
||||||
|
throw new ArcanistUsageException(
|
||||||
|
"'git rebase {$this->onto}' failed. ".
|
||||||
|
"You can abort with 'git rebase --abort', ".
|
||||||
|
"or resolve conflicts and use 'git rebase ".
|
||||||
|
"--continue' to continue forward. After resolving the rebase, ".
|
||||||
|
"run 'arc land' again.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've rebased, the merge-base of origin/master and HEAD may
|
||||||
|
// be different. Reparse the relative commit.
|
||||||
|
$repository_api->parseRelativeLocalCommit(array($this->ontoRemoteBranch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function squash() {
|
||||||
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
$repository_api->execxLocal('checkout %s', $this->onto);
|
||||||
|
|
||||||
|
$repository_api->execxLocal(
|
||||||
|
'merge --squash --ff-only %s',
|
||||||
|
$this->branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function merge() {
|
||||||
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
if (!$use_squash) {
|
|
||||||
// In immutable histories, do a --no-ff merge to force a merge commit with
|
// In immutable histories, do a --no-ff merge to force a merge commit with
|
||||||
// the right message.
|
// the right message.
|
||||||
|
$repository_api->execxLocal('checkout %s', $this->onto);
|
||||||
|
|
||||||
chdir($repository_api->getPath());
|
chdir($repository_api->getPath());
|
||||||
$err = phutil_passthru(
|
$err = phutil_passthru(
|
||||||
'git merge --no-ff --no-commit %s',
|
'git merge --no-ff --no-commit %s',
|
||||||
$branch);
|
$this->branch);
|
||||||
|
|
||||||
if ($err) {
|
if ($err) {
|
||||||
throw new ArcanistUsageException(
|
throw new ArcanistUsageException(
|
||||||
"'git merge' failed. Your working copy has been left in a partially ".
|
"'git merge' failed. Your working copy has been left in a partially ".
|
||||||
"merged state. You can: abort with 'git merge --abort'; or follow ".
|
"merged state. You can: abort with 'git merge --abort'; or follow ".
|
||||||
"the instructions to complete the merge.");
|
"the instructions to complete the merge.");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// In mutable histories, do a --squash merge.
|
|
||||||
$repository_api->execxLocal(
|
|
||||||
'merge --squash --ff-only %s',
|
|
||||||
$branch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function push() {
|
||||||
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
$tmp_file = new TempFile();
|
$tmp_file = new TempFile();
|
||||||
Filesystem::writeFile($tmp_file, $message);
|
Filesystem::writeFile($tmp_file, $this->message);
|
||||||
|
|
||||||
$repository_api->execxLocal(
|
$repository_api->execxLocal(
|
||||||
'commit -F %s',
|
'commit -F %s',
|
||||||
$tmp_file);
|
$tmp_file);
|
||||||
|
@ -296,18 +382,20 @@ EOTEXT
|
||||||
if ($this->getArgument('hold')) {
|
if ($this->getArgument('hold')) {
|
||||||
echo phutil_console_format(
|
echo phutil_console_format(
|
||||||
"Holding change in **%s**: it has NOT been pushed yet.\n",
|
"Holding change in **%s**: it has NOT been pushed yet.\n",
|
||||||
$onto);
|
$this->onto);
|
||||||
} else {
|
} else {
|
||||||
echo "Pushing change...\n\n";
|
echo "Pushing change...\n\n";
|
||||||
|
|
||||||
chdir($repository_api->getPath());
|
chdir($repository_api->getPath());
|
||||||
|
|
||||||
$err = phutil_passthru(
|
$err = phutil_passthru(
|
||||||
'git push %s %s',
|
'git push %s %s',
|
||||||
$remote,
|
$this->remote,
|
||||||
$onto);
|
$this->onto);
|
||||||
|
|
||||||
if ($err) {
|
if ($err) {
|
||||||
throw new ArcanistUsageException("'git push' failed.");
|
$repo_command = $repository_api->getSourceControlSystemName();
|
||||||
|
throw new ArcanistUsageException("'{$repo_command} push' failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$mark_workflow = $this->buildChildWorkflow(
|
$mark_workflow = $this->buildChildWorkflow(
|
||||||
|
@ -315,34 +403,36 @@ EOTEXT
|
||||||
array(
|
array(
|
||||||
'--finalize',
|
'--finalize',
|
||||||
'--quiet',
|
'--quiet',
|
||||||
$revision['id'],
|
$this->revision['id'],
|
||||||
));
|
));
|
||||||
$mark_workflow->run();
|
$mark_workflow->run();
|
||||||
|
|
||||||
echo "\n";
|
echo "\n";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->getArgument('keep-branch')) {
|
private function cleanupBranch() {
|
||||||
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
|
echo "Cleaning up feature branch...\n";
|
||||||
list($ref) = $repository_api->execxLocal(
|
list($ref) = $repository_api->execxLocal(
|
||||||
'rev-parse --verify %s',
|
'rev-parse --verify %s',
|
||||||
$branch);
|
$this->branch);
|
||||||
$ref = trim($ref);
|
$ref = trim($ref);
|
||||||
$recovery_command = csprintf(
|
$recovery_command = csprintf(
|
||||||
'git checkout -b %s %s',
|
'git checkout -b %s %s',
|
||||||
$branch,
|
$this->branch,
|
||||||
$ref);
|
$ref);
|
||||||
|
|
||||||
echo "Cleaning up feature branch...\n";
|
|
||||||
echo "(Use `{$recovery_command}` if you want it back.)\n";
|
echo "(Use `{$recovery_command}` if you want it back.)\n";
|
||||||
$repository_api->execxLocal(
|
$repository_api->execxLocal(
|
||||||
'branch -D %s',
|
'branch -D %s',
|
||||||
$branch);
|
$this->branch);
|
||||||
|
|
||||||
if ($this->getArgument('delete-remote')) {
|
if ($this->getArgument('delete-remote')) {
|
||||||
list($err, $ref) = $repository_api->execManualLocal(
|
list($err, $ref) = $repository_api->execManualLocal(
|
||||||
'rev-parse --verify %s/%s',
|
'rev-parse --verify %s/%s',
|
||||||
$remote,
|
$this->remote,
|
||||||
$branch);
|
$this->branch);
|
||||||
|
|
||||||
if ($err) {
|
if ($err) {
|
||||||
echo "No remote feature branch to clean up.\n";
|
echo "No remote feature branch to clean up.\n";
|
||||||
|
@ -356,28 +446,12 @@ EOTEXT
|
||||||
echo "Cleaning up remote feature branch...\n";
|
echo "Cleaning up remote feature branch...\n";
|
||||||
$repository_api->execxLocal(
|
$repository_api->execxLocal(
|
||||||
'push %s :%s',
|
'push %s :%s',
|
||||||
$remote,
|
$this->remote,
|
||||||
$branch);
|
$this->branch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we were on some branch A and the user ran "arc land B", switch back
|
|
||||||
// to A.
|
|
||||||
if (($old_branch != $branch) && ($old_branch != $onto)) {
|
|
||||||
$repository_api->execxLocal(
|
|
||||||
'checkout %s',
|
|
||||||
$old_branch);
|
|
||||||
echo phutil_console_format(
|
|
||||||
"Switched back to branch **%s**.\n",
|
|
||||||
$old_branch);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Done.\n";
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getSupportedRevisionControlSystems() {
|
protected function getSupportedRevisionControlSystems() {
|
||||||
return array('git');
|
return array('git');
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue