4 // This is the minimalist denialist implementation that doesn't check it's
5 // pre-conditions and will screw up if you don't know what you're doing.
8 * Manage the current working directory as a stack.
13 function __construct($dirs = array()) {
18 $this->dirs
[] = getcwd();
20 throw new Exception("Failed to chdir($dir)");
25 $oldDir = array_pop($this->dirs
);
33 * @var string 'checkout', 'begin', 'help', etc
40 protected $baseBranch;
43 * @var array ($repoName => $gitRef)
50 protected $civiRoot = '.';
55 protected $drupalVersion = 7;
60 protected $dryRun = FALSE;
65 protected $fetch = FALSE;
70 protected $rebase = FALSE;
73 * @var string, the word 'all' or comma-delimited list of repo names
75 protected $repoFilter = 'all';
78 * @var array ($repoName => $relPath)
85 protected $useGencode = FALSE;
90 protected $useSetup = FALSE;
93 * @var array, non-hyphenated arguments after the basedir
98 * @var string, the name of this program
107 function __construct() {
108 $this->dirStack
= new DirStack();
109 $this->repos
= array(
111 'drupal' => 'drupal',
112 'joomla' => 'joomla',
113 'packages' => 'packages',
114 'wordpress' => 'WordPress',
118 function main($args) {
119 if (!$this->parseOptions($args)) {
120 printf("Error parsing arguments\n");
125 // All operations relative to civiRoot
126 $this->dirStack
->push($this->civiRoot
);
128 // Filter branch list based on what repos actually exist
129 foreach (array_keys($this->repos
) as $repo) {
130 if (!is_dir($this->repos
[$repo])) {
131 unset($this->repos
[$repo]);
134 if (!isset($this->repos
['core']) ||
!isset($this->repos
['packages'])) {
135 return $this->returnError("Root appears to be invalid -- missing too many repos. Try --root=<dir>\n");
138 $this->repos
= $this->filterRepos($this->repoFilter
, $this->repos
);
141 switch ($this->action
) {
143 call_user_func_array(array($this, 'doCheckoutAll'), $this->arguments
);
146 call_user_func_array(array($this, 'doFetchAll'), $this->arguments
);
149 call_user_func_array(array($this, 'doStatusAll'), $this->arguments
);
152 call_user_func_array(array($this, 'doBegin'), $this->arguments
);
155 call_user_func_array(array($this, 'doResume'), $this->arguments
);
157 //case 'merge-forward':
158 // call_user_func_array(array($this, 'doMergeForward'), $this->arguments);
161 call_user_func_array(array($this, 'doPush'), $this->arguments
);
168 return $this->returnError("unrecognized action: {$this->action}\n");
171 if ($this->useSetup
) {
172 $this->run('core', $this->civiRoot
. '/bin', 'bash', 'setup.sh');
174 elseif ($this->useGencode
) {
175 $this->run('core', $this->civiRoot
. '/xml', 'php', 'GenCode.php');
178 $this->dirStack
->pop();
185 function parseOptions($args) {
186 $this->branches
= array();
187 $this->arguments
= array();
189 foreach ($args as $arg) {
190 if ($arg == '--fetch') {
193 elseif ($arg == '--rebase') {
194 $this->rebase
= TRUE;
196 elseif ($arg == '--dry-run' ||
$arg == '-n') {
197 $this->dryRun
= TRUE;
199 elseif ($arg == '--gencode') {
200 $this->useGencode
= TRUE;
202 elseif ($arg == '--setup') {
203 $this->useSetup
= TRUE;
205 elseif (preg_match('/^--d([678])/', $arg, $matches)) {
206 $this->drupalVersion
= $matches[1];
208 elseif (preg_match('/^--root=(.*)/', $arg, $matches)) {
209 $this->civiRoot
= $matches[1];
211 elseif (preg_match('/^--repos=(.*)/', $arg, $matches)) {
212 $this->repoFilter
= $matches[1];
214 elseif (preg_match('/^--(core|packages|joomla|drupal|wordpress)=(.*)/', $arg, $matches)) {
215 $this->branches
[$matches[1]] = $matches[2];
217 elseif (preg_match('/^-/', $arg)) {
218 printf("unrecognized argument: %s\n", $arg);
222 $this->arguments
[] = $arg;
226 $this->program
= @array_shift
($this->arguments
);
227 $this->action
= @array_shift
($this->arguments
);
232 $program = basename($this->program
);
233 echo "Givi - Coordinate git checkouts across CiviCRM repositories\n";
235 echo " You have cloned and forked the CiviCRM repos. Each of the repos has two\n";
236 echo " remotes (origin + upstream). When working on a new PR, you generally want\n";
237 echo " to checkout official code (eg upstream/master) in all repos, but 1-2 repos\n";
238 echo " should use a custom branch (which tracks upstream/master).\n";
240 echo " $program [options] checkout <branch>\n";
241 echo " $program [options] fetch\n";
242 echo " $program [options] status\n";
243 echo " $program [options] begin <base-branch> [--core=<new-branch>|--drupal=<new-branch>|...] \n";
244 echo " $program [options] resume [--rebase] <base-branch> [--core=<custom-branch>|--drupal=<custom-branch>|...] \n";
245 #echo " $program [options] merge-forward <maintenace-branch> <development-branch>\n";
246 #echo " $program [options] push <remote> <branch>[:<branch>]\n";
248 echo " checkout: Checkout same branch name on all repos\n";
249 echo " fetch: Fetch remote changes on all repos\n";
250 echo " status: Display status on all repos\n";
251 echo " begin: Begin work on a new branch on some repo (and use base-branch for all others)\n";
252 echo " resume: Resume work on an existing branch on some repo (and use base-branch for all others)\n";
253 #echo " merge-forward: On each repo, merge changes from maintenance branch to development branch\n";
254 #echo " push: On each repo, push a branch to a remote (Note: only intended for use with merge-forward)\n";
255 echo "Common options:\n";
256 echo " --dry-run: Don't do anything; only print commands that would be run\n";
257 echo " --d6: Specify that Drupal branches should use 6.x-* prefixes\n";
258 echo " --d7: Specify that Drupal branches should use 7.x-* prefixes (default)\n";
259 echo " --fetch: Fetch the latest code before creating, updating, or checking-out anything\n";
260 echo " --repos=X: Restrict operations to the listed repos (comma-delimited list) (default: all)";
261 echo " --root=X: Specify CiviCRM root directory (default: .)\n";
262 echo " --gencode: Run xml/GenCode after checking out code\n";
263 echo " --setup: Run bin/setup.sh (incl xml/GenCode) after checking out code\n";
264 echo "Special options:\n";
265 echo " --core=X: Specify the branch to use on the core repository\n";
266 echo " --packages=X: Specify the branch to use on the packages repository\n";
267 echo " --drupal=X: Specify the branch to use on the drupal repository\n";
268 echo " --joomla=X: Specify the branch to use on the joomla repository\n";
269 echo " --wordpress=X: Specify the branch to use on the wordpress repository\n";
270 echo " --rebase: Perform a rebase before starting work\n";
271 echo "Known repositories:\n";
272 foreach ($this->repos
as $repo => $relPath) {
273 printf(" %-12s: %s\n", $repo, realpath($this->civiRoot
. DIRECTORY_SEPARATOR
. $relPath));
275 echo "When using 'begin' or 'resume' with a remote base-branch, most repositories\n";
276 echo "will have a detached HEAD. Only repos with an explicit branch will be real,\n";
277 echo "local branches.\n";
280 function doCheckoutAll($baseBranch = NULL) {
282 return $this->returnError("Missing <branch>\n");
284 $branches = $this->resolveBranches($baseBranch, $this->branches
);
289 foreach ($this->repos
as $repo => $relPath) {
290 $filteredBranch = $this->filterBranchName($repo, $branches[$repo]);
291 $this->run($repo, $relPath, 'git', 'checkout', $filteredBranch);
296 function doStatusAll() {
297 foreach ($this->repos
as $repo => $relPath) {
298 $this->run($repo, $relPath, 'git', 'status');
303 function doBegin($baseBranch = NULL) {
305 return $this->returnError("Missing <base-branch>\n");
307 if (empty($this->branches
)) {
308 return $this->returnError("Must specify a custom branch for at least one repository.\n");
310 $branches = $this->resolveBranches($baseBranch, $this->branches
);
315 foreach ($this->repos
as $repo => $relPath) {
316 $filteredBranch = $this->filterBranchName($repo, $branches[$repo]);
317 $filteredBaseBranch = $this->filterBranchName($repo, $baseBranch);
319 if ($filteredBranch == $filteredBaseBranch) {
320 $this->run($repo, $relPath, 'git', 'checkout', $filteredBranch);
323 $this->run($repo, $relPath, 'git', 'checkout', '-b', $filteredBranch, $filteredBaseBranch);
328 function doResume($baseBranch = NULL) {
330 return $this->returnError("Missing <base-branch>\n");
332 if (empty($this->branches
)) {
333 return $this->returnError("Must specify a custom branch for at least one repository.\n");
335 $branches = $this->resolveBranches($baseBranch, $this->branches
);
340 foreach ($this->repos
as $repo => $relPath) {
341 $filteredBranch = $this->filterBranchName($repo, $branches[$repo]);
342 $filteredBaseBranch = $this->filterBranchName($repo, $baseBranch);
344 $this->run($repo, $relPath, 'git', 'checkout', $filteredBranch);
345 if ($filteredBranch != $filteredBaseBranch && $this->rebase
) {
346 list ($baseRemoteRepo, $baseRemoteBranch) = $this->parseBranchRepo($filteredBaseBranch);
347 $this->run($repo, $relPath, 'git', 'pull', '--rebase', $baseRemoteRepo, $baseRemoteBranch);
354 If we want merge-forward changes to be subject to PR process, then this
355 should useful. Currently using a simpler process based on
356 toosl/scripts/merge-forward
358 function doMergeForward($maintBranch, $devBranch) {
360 return $this->returnError("Missing <maintenace-base-branch>\n");
363 return $this->returnError("Missing <development-base-branch>\n");
365 list ($maintBranchRepo, $maintBranchName) = $this->parseBranchRepo($maintBranch);
366 list ($devBranchRepo, $devBranchName) = $this->parseBranchRepo($devBranch);
368 $newBranchRepo = $devBranchRepo;
369 $newBranchName = $maintBranchName . '-' . $devBranchName . '-' . date('Y-m-d-H-i-s');
375 foreach ($this->repos as $repo => $relPath) {
376 $filteredMaintBranch = $this->filterBranchName($repo, $maintBranch);
377 $filteredDevBranch = $this->filterBranchName($repo, $devBranch);
378 $filteredNewBranchName = $this->filterBranchName($repo, $newBranchName);
380 $this->run($repo, $relPath, 'git', 'checkout', '-b', $filteredNewBranchName, $filteredDevBranch);
381 $this->run($repo, $relPath, 'git', 'merge', $filteredMaintBranch);
386 function doPush($newBranchRepo, $newBranchNames) {
387 if (!$newBranchRepo) {
388 return $this->returnError("Missing <remote>\n");
390 if (!$newBranchNames) {
391 return $this->returnError("Missing <branch>[:<branch>]\n");
393 if (FALSE !== strpos($newBranchNames, ':')) {
394 list ($newBranchFromName,$newBranchToName) = explode(':', $newBranchNames);
395 foreach ($this->repos
as $repo => $relPath) {
396 $filteredFromName = $this->filterBranchName($repo, $newBranchFromName);
397 $filteredToName = $this->filterBranchName($repo, $newBranchToName);
399 $this->run($repo, $relPath, 'git', 'push', $newBranchRepo, $filteredFromName . ':' . $filteredToName);
402 foreach ($this->repos
as $repo => $relPath) {
403 $filteredName = $this->filterBranchName($repo, $newBranchNames);
404 $this->run($repo, $relPath, 'git', 'push', $newBranchRepo, $filteredName);
411 * Given a ref name, determine the repo and branch
413 * FIXME: only supports $refs like "foo" (implicit origin) or "myremote/foo"
418 function parseBranchRepo($ref, $defaultRemote = 'origin') {
419 $parts = explode('/', $ref);
420 if (count($parts) == 1) {
421 return array($defaultRemote, $parts[1]);
423 elseif (count($parts) == 2) {
427 throw new Exception("Failed to parse branch name ($ref)");
434 * Any items after $command will be escaped and added to $command
436 * @param string $runDir
437 * @param string $command
440 function run($repoName, $runDir, $command) {
441 $this->dirStack
->push($runDir);
443 $args = func_get_args();
447 foreach ($args as $arg) {
448 $command .= ' ' . escapeshellarg($arg);
450 printf("\n\n\nRUN [%s]: %s\n", $repoName, $command);
454 $r = system($command);
457 $this->dirStack
->pop();
461 function doFetchAll() {
462 foreach ($this->repos
as $repo => $relPath) {
463 $this->run($repo, $relPath, 'git', 'fetch', '--all');
468 * @param string $default branch to use by default
469 * @return array ($repoName => $gitRef)
471 function resolveBranches($default, $overrides) {
472 $branches = $overrides;
473 foreach ($this->repos
as $repo => $relPath) {
474 if (!isset($branches[$repo])) {
475 $branches[$repo] = $default;
481 function filterBranchName($repoName, $branchName) {
482 if ($branchName == '') {
485 if ($repoName == 'drupal') {
486 $parts = explode('/', $branchName);
487 $last = $this->drupalVersion
. '.x-' . array_pop($parts);
488 array_push($parts, $last);
489 return implode('/', $parts);
495 * @param string $filter e.g. "all" or "repo1,repo2"
496 * @param array $repos ($repoName => $repoDir)
497 * @return array ($repoName => $repoDir)
499 function filterRepos($filter, $repos) {
500 if ($filter == 'all') {
504 $inclRepos = explode(',', $filter);
505 $unknowns = array_diff($inclRepos, array_keys($repos));
506 if (!empty($unknowns)) {
507 throw new Exception("Unknown Repos: " . implode(',', $unknowns));
509 $unwanted = array_diff(array_keys($repos), $inclRepos);
510 foreach ($unwanted as $repo) {
511 unset($repos[$repo]);
516 function returnError($message) {
517 echo "ERROR: ", $message, "\n";