From 6130de47ec296bbf6a636bfefa074825f4c06592 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 12 Mar 2013 07:17:46 -0400 Subject: [PATCH] givi - Add helper to coordinate checkouts across all repos --- bin/givi.php | 379 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100755 bin/givi.php diff --git a/bin/givi.php b/bin/givi.php new file mode 100755 index 0000000000..9e5bccccf2 --- /dev/null +++ b/bin/givi.php @@ -0,0 +1,379 @@ +#!/usr/bin/env php +dirs = $dirs; + } + + function push($dir) { + $this->dirs[] = getcwd(); + if (!chdir($dir)) { + throw new Exception("Failed to chdir($dir)"); + } + } + + function pop() { + $oldDir = array_pop($this->dirs); + chdir($oldDir); + } +} + +class Givi { + + /** + * @var string 'checkout-all', 'begin', 'help', etc + */ + protected $action; + + /** + * @var string + */ + protected $baseBranch; + + /** + * @var array ($repoName => $gitRef) + */ + protected $branches; + + /** + * @var string + */ + protected $civiRoot = '.'; + + /** + * @var int + */ + protected $drupalVersion = 7; + + /** + * @var bool + */ + protected $dryRun = FALSE; + + /** + * @var bool + */ + protected $fetch = FALSE; + + /** + * @var bool + */ + protected $rebase = FALSE; + + /** + * @var array ($repoName => $relPath) + */ + protected $repos; + + /** + * @var array, non-hyphenated arguments after the basedir + */ + protected $arguments; + + /** + * @var string, the name of this program + */ + protected $program; + + /** + * @var DirStack + */ + protected $dirStack; + + function __construct() { + $this->dirStack = new DirStack(); + $this->repos = array( + 'core' => '.', + 'packages' => 'packages', + 'joomla' => 'joomla', + 'drupal' => 'drupal', + 'wordpress' => 'WordPress', + ); + } + + function main($args) { + if (!$this->parseOptions($args)) { + printf("Error parsing arguments\n"); + $this->doHelp(); + return FALSE; + } + + // All operations relative to civiRoot + $this->dirStack->push($this->civiRoot); + + // Filter branch list based on what repos actually exist + foreach (array_keys($this->repos) as $repo) { + if (!is_dir($this->repos[$repo])) { + unset($this->repos[$repo]); + } + } + if (!isset($this->repos['core']) || !isset($this->repos['packages'])) { + return $this->returnError("Root appears to be invalid -- missing too many repos. Try --root=\n"); + } + + // Run the action + switch ($this->action) { + case 'checkout-all': + call_user_func_array(array($this, 'doCheckoutAll'), $this->arguments); + break; + case 'fetch-all': + call_user_func_array(array($this, 'doFetchAll'), $this->arguments); + break; + case 'status-all': + call_user_func_array(array($this, 'doStatusAll'), $this->arguments); + break; + case 'begin': + call_user_func_array(array($this, 'doBegin'), $this->arguments); + break; + case 'resume': + call_user_func_array(array($this, 'doResume'), $this->arguments); + break; + case 'help': + $this->doHelp(); + break; + default: + return $this->returnError("unrecognized action: {$this->action}\n"); + } + + $this->dirStack->pop(); + } + + /** + * @param $args + * @return bool + */ + function parseOptions($args) { + $this->branches = array(); + $this->arguments = array(); + + foreach ($args as $arg) { + if ($arg == '--fetch') { + $this->fetch = TRUE; + } + elseif ($arg == '--rebase') { + $this->rebase = TRUE; + } + elseif ($arg == '--dry-run' || $arg == '-n') { + $this->dryRun = TRUE; + } + elseif (preg_match('/^--d([678])/', $arg, $matches)) { + $this->drupalVersion = $matches[1]; + } + elseif (preg_match('/^--root=(.*)/', $arg, $matches)) { + $this->civiRoot = $matches[1]; + } + elseif (preg_match('/^--(core|packages|joomla|drupal|wordpress)=(.*)/', $arg, $matches)) { + $this->branches[$matches[1]] = $matches[2]; + } + elseif (preg_match('/^-/', $arg)) { + printf("unrecognized argument: %s\n", $arg); + return FALSE; + } + else { + $this->arguments[] = $arg; + } + } + + $this->program = @array_shift($this->arguments); + $this->action = @array_shift($this->arguments); + return TRUE; + } + + function doHelp() { + $program = basename($this->program); + echo "Givi - Coordinate git checkouts across CiviCRM repositories\n"; + echo "Usage:\n"; + echo " $program [options] checkout-all \n"; + echo " $program [options] fetch-all\n"; + echo " $program [options] status-all\n"; + echo " $program [options] begin [--core=|--drupal=|...] \n"; + echo " $program [options] resume [--rebase] [--core=|--drupal=|...] \n"; + echo "Actions:\n"; + echo " checkout-all: Checkout same branch name on all repos\n"; + echo " status-all: Display status on all repos\n"; + echo " begin: Begin work on a new branch on some repo (and use base-branch for all others)\n"; + echo " resume: Resume work on an existing branch on some repo (and use base-branch for all others)\n"; + echo "Common options:\n"; + echo " --d6: Specify that Drupal branches should use 6.x-* prefixes\n"; + echo " --d7: Specify that Drupal branches should use 7.x-* prefixes (default)\n"; + echo " --fetch: Fetch the latest code before creating, updating, or checking-out anything\n"; + echo " --root=X: Specify CiviCRM root directory (default: .)\n"; + echo "Special options:\n"; + echo " --core=X: Specify the branch to use on the core repository\n"; + echo " --packages=X: Specify the branch to use on the packages repository\n"; + echo " --drupal=X: Specify the branch to use on the drupal repository\n"; + echo " --joomla=X: Specify the branch to use on the joomla repository\n"; + echo " --wordpress=X: Specify the branch to use on the wordpress repository\n"; + echo " --rebase: Perform a rebase before starting work\n"; + echo "When using 'begin' or 'resume' with a remote base-branch, most repositories\n"; + echo "will have a detached HEAD. Only repos with an explicit branch will be real,\n"; + echo "local branches.\n"; + } + + function doCheckoutAll($baseBranch = NULL) { + if (!$baseBranch) { + return $this->returnError("Missing \n"); + } + $branches = $this->resolveBranches($baseBranch, $this->branches); + if ($this->fetch) { + $this->doFetchAll(); + } + + foreach ($this->repos as $repo => $relPath) { + $filteredBranch = $this->filterBranchName($repo, $branches[$repo]); + $this->run($relPath, 'git', 'checkout', $filteredBranch); + } + return TRUE; + } + + function doStatusAll() { + foreach ($this->repos as $repo => $relPath) { + $this->run($relPath, 'git', 'status'); + } + return TRUE; + } + + function doBegin($baseBranch = NULL) { + if (!$baseBranch) { + return $this->returnError("Missing \n"); + } + if (empty($this->branches)) { + return $this->returnError("Must specify a custom branch for at least one repository.\n"); + } + $branches = $this->resolveBranches($baseBranch, $this->branches); + if ($this->fetch) { + $this->doFetchAll(); + } + + foreach ($this->repos as $repo => $relPath) { + $filteredBranch = $this->filterBranchName($repo, $branches[$repo]); + $filteredBaseBranch = $this->filterBranchName($repo, $baseBranch); + if ($filteredBranch == $filteredBaseBranch) { + $this->run($relPath, 'git', 'checkout', $filteredBranch); + } + else { + $this->run($relPath, 'git', 'checkout', '-b', $filteredBranch, $filteredBaseBranch); + } + } + } + + function doResume($baseBranch = NULL) { + if (!$baseBranch) { + return $this->returnError("Missing \n"); + } + if (empty($this->branches)) { + return $this->returnError("Must specify a custom branch for at least one repository.\n"); + } + $branches = $this->resolveBranches($baseBranch, $this->branches); + if ($this->fetch) { + $this->doFetchAll(); + } + + foreach ($this->repos as $repo => $relPath) { + $this->run($relPath, 'git', 'checkout', $branches[$repo]); + if ($branches[$repo] != $baseBranch && $this->rebase) { + // FIXME: assumes + list ($baseRemoteRepo, $baseRemoteBranch) = $this->parseBranchRepo($baseBranch); + $this->run($relPath, 'git', 'pull', '--rebase', $baseRemoteRepo, $baseRemoteBranch); + } + } + } + + /** + * Given a ref name, determine the repo and branch + * + * FIXME: only supports $refs like "foo" (implicit origin) or "myremote/foo" + * + * @param $ref + * @return array + */ + function parseBranchRepo($ref, $defaultRemote = 'origin') { + $parts = explode('/', $ref); + if (count($parts) == 1) { + return array($defaultRemote, $parts[1]); + } + elseif (count($parts) == 2) { + return $parts; + } + else { + throw new Exception("Failed to parse branch name ($ref)"); + } + } + + /** + * Run a command + * + * Any items after $command will be escaped and added to $command + * + * @param string $runDir + * @param string $command + * @return string + */ + function run($runDir, $command) { + $this->dirStack->push($runDir); + + $args = func_get_args(); + array_shift($args); + array_shift($args); + foreach ($args as $arg) { + $command .= ' ' . escapeshellarg($arg); + } + printf("\nRUN [%s]: %s\n", $runDir, $command); + if ($this->dryRun) { + $r = NULL; + } else { + $r = system($command); + } + + $this->dirStack->pop(); + return $r; + } + + function doFetchAll() { + foreach ($this->repos as $repo => $relPath) { + $this->run($relPath, 'git', 'fetch', '--all'); + } + } + + /** + * @param string $default branch to use by default + * @return array ($repoName => $gitRef) + */ + function resolveBranches($default, $overrides) { + $branches = $overrides; + foreach ($this->repos as $repo => $relPath) { + if (!isset($branches[$repo])) { + $branches[$repo] = $default; + } + } + return $branches; + } + + function filterBranchName($repoName, $branchName) { + if ($repoName == 'drupal') { + $parts = explode('/', $branchName); + $last = $this->drupalVersion . '.x-' . array_pop($parts); + array_push($parts, $last); + return implode('/', $parts); + } + return $branchName; + } + + function returnError($message) { + echo "ERROR: ", $message, "\n"; + $this->doHelp(); + return FALSE; + } +} + +$givi = new Givi(); +$givi->main($argv); -- 2.25.1