Merge pull request #437 from kurund/updateCMSEmail
[civicrm-core.git] / bin / givi
CommitLineData
6130de47
TO
1#!/usr/bin/env php
2<?php
3
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.
6
7/**
8 * Manage the current working directory as a stack.
9 */
10class DirStack {
11 protected $dirs;
12
13 function __construct($dirs = array()) {
14 $this->dirs = $dirs;
15 }
16
17 function push($dir) {
18 $this->dirs[] = getcwd();
19 if (!chdir($dir)) {
20 throw new Exception("Failed to chdir($dir)");
21 }
22 }
23
24 function pop() {
25 $oldDir = array_pop($this->dirs);
26 chdir($oldDir);
27 }
28}
29
30class Givi {
31
32 /**
749e432e 33 * @var string 'checkout', 'begin', 'help', etc
6130de47
TO
34 */
35 protected $action;
36
37 /**
38 * @var string
39 */
40 protected $baseBranch;
41
42 /**
43 * @var array ($repoName => $gitRef)
44 */
45 protected $branches;
46
47 /**
48 * @var string
49 */
50 protected $civiRoot = '.';
51
52 /**
53 * @var int
54 */
55 protected $drupalVersion = 7;
56
57 /**
58 * @var bool
59 */
60 protected $dryRun = FALSE;
61
62 /**
63 * @var bool
64 */
65 protected $fetch = FALSE;
66
67 /**
68 * @var bool
69 */
70 protected $rebase = FALSE;
71
8d64bbe0
TO
72 /**
73 * @var string, the word 'all' or comma-delimited list of repo names
74 */
75 protected $repoFilter = 'all';
76
6130de47
TO
77 /**
78 * @var array ($repoName => $relPath)
79 */
80 protected $repos;
81
4384f1bb
TO
82 /**
83 * @var bool
84 */
85 protected $useGencode = FALSE;
86
87 /**
88 * @var bool
89 */
90 protected $useSetup = FALSE;
91
6130de47
TO
92 /**
93 * @var array, non-hyphenated arguments after the basedir
94 */
95 protected $arguments;
96
97 /**
98 * @var string, the name of this program
99 */
100 protected $program;
101
102 /**
103 * @var DirStack
104 */
105 protected $dirStack;
106
107 function __construct() {
108 $this->dirStack = new DirStack();
109 $this->repos = array(
110 'core' => '.',
6130de47 111 'drupal' => 'drupal',
4842290b
TO
112 'joomla' => 'joomla',
113 'packages' => 'packages',
6130de47
TO
114 'wordpress' => 'WordPress',
115 );
116 }
117
118 function main($args) {
119 if (!$this->parseOptions($args)) {
120 printf("Error parsing arguments\n");
121 $this->doHelp();
122 return FALSE;
123 }
124
125 // All operations relative to civiRoot
126 $this->dirStack->push($this->civiRoot);
127
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]);
132 }
133 }
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");
136 }
137
8d64bbe0
TO
138 $this->repos = $this->filterRepos($this->repoFilter, $this->repos);
139
6130de47
TO
140 // Run the action
141 switch ($this->action) {
749e432e 142 case 'checkout':
6130de47
TO
143 call_user_func_array(array($this, 'doCheckoutAll'), $this->arguments);
144 break;
749e432e 145 case 'fetch':
6130de47
TO
146 call_user_func_array(array($this, 'doFetchAll'), $this->arguments);
147 break;
749e432e 148 case 'status':
6130de47
TO
149 call_user_func_array(array($this, 'doStatusAll'), $this->arguments);
150 break;
151 case 'begin':
152 call_user_func_array(array($this, 'doBegin'), $this->arguments);
153 break;
154 case 'resume':
155 call_user_func_array(array($this, 'doResume'), $this->arguments);
156 break;
8d64bbe0
TO
157 //case 'merge-forward':
158 // call_user_func_array(array($this, 'doMergeForward'), $this->arguments);
159 // break;
160 case 'push':
161 call_user_func_array(array($this, 'doPush'), $this->arguments);
162 break;
6130de47 163 case 'help':
03da1773 164 case '':
6130de47
TO
165 $this->doHelp();
166 break;
167 default:
168 return $this->returnError("unrecognized action: {$this->action}\n");
169 }
170
4384f1bb
TO
171 if ($this->useSetup) {
172 $this->run('core', $this->civiRoot . '/bin', 'bash', 'setup.sh');
173 }
174 elseif ($this->useGencode) {
175 $this->run('core', $this->civiRoot . '/xml', 'php', 'GenCode.php');
176 }
177
6130de47
TO
178 $this->dirStack->pop();
179 }
180
181 /**
182 * @param $args
183 * @return bool
184 */
185 function parseOptions($args) {
186 $this->branches = array();
187 $this->arguments = array();
188
189 foreach ($args as $arg) {
190 if ($arg == '--fetch') {
191 $this->fetch = TRUE;
192 }
193 elseif ($arg == '--rebase') {
194 $this->rebase = TRUE;
195 }
196 elseif ($arg == '--dry-run' || $arg == '-n') {
197 $this->dryRun = TRUE;
198 }
4384f1bb
TO
199 elseif ($arg == '--gencode') {
200 $this->useGencode = TRUE;
201 }
202 elseif ($arg == '--setup') {
203 $this->useSetup = TRUE;
204 }
6130de47
TO
205 elseif (preg_match('/^--d([678])/', $arg, $matches)) {
206 $this->drupalVersion = $matches[1];
207 }
208 elseif (preg_match('/^--root=(.*)/', $arg, $matches)) {
209 $this->civiRoot = $matches[1];
210 }
8d64bbe0
TO
211 elseif (preg_match('/^--repos=(.*)/', $arg, $matches)) {
212 $this->repoFilter = $matches[1];
213 }
6130de47
TO
214 elseif (preg_match('/^--(core|packages|joomla|drupal|wordpress)=(.*)/', $arg, $matches)) {
215 $this->branches[$matches[1]] = $matches[2];
216 }
217 elseif (preg_match('/^-/', $arg)) {
218 printf("unrecognized argument: %s\n", $arg);
219 return FALSE;
220 }
221 else {
222 $this->arguments[] = $arg;
223 }
224 }
225
226 $this->program = @array_shift($this->arguments);
227 $this->action = @array_shift($this->arguments);
228 return TRUE;
229 }
230
231 function doHelp() {
232 $program = basename($this->program);
233 echo "Givi - Coordinate git checkouts across CiviCRM repositories\n";
729ccb4d
TO
234 echo "Scenario:\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";
6130de47 239 echo "Usage:\n";
749e432e
TO
240 echo " $program [options] checkout <branch>\n";
241 echo " $program [options] fetch\n";
242 echo " $program [options] status\n";
6130de47
TO
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";
8d64bbe0
TO
245 #echo " $program [options] merge-forward <maintenace-branch> <development-branch>\n";
246 #echo " $program [options] push <remote> <branch>[:<branch>]\n";
6130de47 247 echo "Actions:\n";
749e432e
TO
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";
6130de47
TO
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";
8d64bbe0
TO
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";
6130de47 255 echo "Common options:\n";
749e432e 256 echo " --dry-run: Don't do anything; only print commands that would be run\n";
6130de47
TO
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";
8d64bbe0 260 echo " --repos=X: Restrict operations to the listed repos (comma-delimited list) (default: all)";
6130de47 261 echo " --root=X: Specify CiviCRM root directory (default: .)\n";
4384f1bb
TO
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";
6130de47
TO
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";
ab79d84c
TO
271 echo "Known repositories:\n";
272 foreach ($this->repos as $repo => $relPath) {
273 printf(" %-12s: %s\n", $repo, realpath($this->civiRoot . DIRECTORY_SEPARATOR . $relPath));
274 }
6130de47
TO
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";
278 }
279
280 function doCheckoutAll($baseBranch = NULL) {
281 if (!$baseBranch) {
282 return $this->returnError("Missing <branch>\n");
283 }
284 $branches = $this->resolveBranches($baseBranch, $this->branches);
285 if ($this->fetch) {
286 $this->doFetchAll();
287 }
288
289 foreach ($this->repos as $repo => $relPath) {
290 $filteredBranch = $this->filterBranchName($repo, $branches[$repo]);
8d64bbe0 291 $this->run($repo, $relPath, 'git', 'checkout', $filteredBranch);
6130de47
TO
292 }
293 return TRUE;
294 }
295
296 function doStatusAll() {
297 foreach ($this->repos as $repo => $relPath) {
8d64bbe0 298 $this->run($repo, $relPath, 'git', 'status');
6130de47
TO
299 }
300 return TRUE;
301 }
302
303 function doBegin($baseBranch = NULL) {
304 if (!$baseBranch) {
305 return $this->returnError("Missing <base-branch>\n");
306 }
307 if (empty($this->branches)) {
308 return $this->returnError("Must specify a custom branch for at least one repository.\n");
309 }
310 $branches = $this->resolveBranches($baseBranch, $this->branches);
311 if ($this->fetch) {
312 $this->doFetchAll();
313 }
314
315 foreach ($this->repos as $repo => $relPath) {
316 $filteredBranch = $this->filterBranchName($repo, $branches[$repo]);
317 $filteredBaseBranch = $this->filterBranchName($repo, $baseBranch);
9476bf6f 318
6130de47 319 if ($filteredBranch == $filteredBaseBranch) {
8d64bbe0 320 $this->run($repo, $relPath, 'git', 'checkout', $filteredBranch);
6130de47
TO
321 }
322 else {
8d64bbe0 323 $this->run($repo, $relPath, 'git', 'checkout', '-b', $filteredBranch, $filteredBaseBranch);
6130de47
TO
324 }
325 }
326 }
327
328 function doResume($baseBranch = NULL) {
329 if (!$baseBranch) {
330 return $this->returnError("Missing <base-branch>\n");
331 }
332 if (empty($this->branches)) {
333 return $this->returnError("Must specify a custom branch for at least one repository.\n");
334 }
335 $branches = $this->resolveBranches($baseBranch, $this->branches);
336 if ($this->fetch) {
337 $this->doFetchAll();
338 }
339
340 foreach ($this->repos as $repo => $relPath) {
9476bf6f
TO
341 $filteredBranch = $this->filterBranchName($repo, $branches[$repo]);
342 $filteredBaseBranch = $this->filterBranchName($repo, $baseBranch);
343
8d64bbe0 344 $this->run($repo, $relPath, 'git', 'checkout', $filteredBranch);
9476bf6f
TO
345 if ($filteredBranch != $filteredBaseBranch && $this->rebase) {
346 list ($baseRemoteRepo, $baseRemoteBranch) = $this->parseBranchRepo($filteredBaseBranch);
8d64bbe0 347 $this->run($repo, $relPath, 'git', 'pull', '--rebase', $baseRemoteRepo, $baseRemoteBranch);
6130de47
TO
348 }
349 }
350 }
351
8d64bbe0
TO
352 /*
353
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
357
358 function doMergeForward($maintBranch, $devBranch) {
359 if (!$maintBranch) {
360 return $this->returnError("Missing <maintenace-base-branch>\n");
361 }
362 if (!$devBranch) {
363 return $this->returnError("Missing <development-base-branch>\n");
364 }
365 list ($maintBranchRepo, $maintBranchName) = $this->parseBranchRepo($maintBranch);
366 list ($devBranchRepo, $devBranchName) = $this->parseBranchRepo($devBranch);
367
368 $newBranchRepo = $devBranchRepo;
369 $newBranchName = $maintBranchName . '-' . $devBranchName . '-' . date('Y-m-d-H-i-s');
370
371 if ($this->fetch) {
372 $this->doFetchAll();
373 }
374
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);
379
380 $this->run($repo, $relPath, 'git', 'checkout', '-b', $filteredNewBranchName, $filteredDevBranch);
381 $this->run($repo, $relPath, 'git', 'merge', $filteredMaintBranch);
382 }
383 }
384 */
385
386 function doPush($newBranchRepo, $newBranchNames) {
387 if (!$newBranchRepo) {
388 return $this->returnError("Missing <remote>\n");
389 }
390 if (!$newBranchNames) {
391 return $this->returnError("Missing <branch>[:<branch>]\n");
392 }
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);
398
399 $this->run($repo, $relPath, 'git', 'push', $newBranchRepo, $filteredFromName . ':' . $filteredToName);
400 }
401 } else {
402 foreach ($this->repos as $repo => $relPath) {
403 $filteredName = $this->filterBranchName($repo, $newBranchNames);
404 $this->run($repo, $relPath, 'git', 'push', $newBranchRepo, $filteredName);
405 }
406 }
407
408 }
409
6130de47
TO
410 /**
411 * Given a ref name, determine the repo and branch
412 *
413 * FIXME: only supports $refs like "foo" (implicit origin) or "myremote/foo"
414 *
415 * @param $ref
416 * @return array
417 */
418 function parseBranchRepo($ref, $defaultRemote = 'origin') {
419 $parts = explode('/', $ref);
420 if (count($parts) == 1) {
421 return array($defaultRemote, $parts[1]);
422 }
423 elseif (count($parts) == 2) {
424 return $parts;
425 }
426 else {
427 throw new Exception("Failed to parse branch name ($ref)");
428 }
429 }
430
431 /**
432 * Run a command
433 *
434 * Any items after $command will be escaped and added to $command
435 *
436 * @param string $runDir
437 * @param string $command
438 * @return string
439 */
8d64bbe0 440 function run($repoName, $runDir, $command) {
6130de47
TO
441 $this->dirStack->push($runDir);
442
443 $args = func_get_args();
444 array_shift($args);
445 array_shift($args);
8d64bbe0 446 array_shift($args);
6130de47
TO
447 foreach ($args as $arg) {
448 $command .= ' ' . escapeshellarg($arg);
449 }
8d64bbe0 450 printf("\n\n\nRUN [%s]: %s\n", $repoName, $command);
6130de47
TO
451 if ($this->dryRun) {
452 $r = NULL;
453 } else {
454 $r = system($command);
455 }
456
457 $this->dirStack->pop();
458 return $r;
459 }
460
461 function doFetchAll() {
462 foreach ($this->repos as $repo => $relPath) {
8d64bbe0 463 $this->run($repo, $relPath, 'git', 'fetch', '--all');
6130de47
TO
464 }
465 }
466
467 /**
468 * @param string $default branch to use by default
469 * @return array ($repoName => $gitRef)
470 */
471 function resolveBranches($default, $overrides) {
472 $branches = $overrides;
473 foreach ($this->repos as $repo => $relPath) {
474 if (!isset($branches[$repo])) {
475 $branches[$repo] = $default;
476 }
477 }
478 return $branches;
479 }
480
481 function filterBranchName($repoName, $branchName) {
8d64bbe0
TO
482 if ($branchName == '') {
483 return '';
484 }
6130de47
TO
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);
490 }
491 return $branchName;
492 }
493
8d64bbe0
TO
494 /**
495 * @param string $filter e.g. "all" or "repo1,repo2"
496 * @param array $repos ($repoName => $repoDir)
497 * @return array ($repoName => $repoDir)
498 */
499 function filterRepos($filter, $repos) {
500 if ($filter == 'all') {
501 return $repos;
502 }
503
504 $inclRepos = explode(',', $filter);
505 $unknowns = array_diff($inclRepos, array_keys($repos));
506 if (!empty($unknowns)) {
507 throw new Exception("Unknown Repos: " . implode(',', $unknowns));
508 }
509 $unwanted = array_diff(array_keys($repos), $inclRepos);
510 foreach ($unwanted as $repo) {
511 unset($repos[$repo]);
512 }
513 return $repos;
514 }
515
6130de47
TO
516 function returnError($message) {
517 echo "ERROR: ", $message, "\n";
518 $this->doHelp();
519 return FALSE;
520 }
521}
522
523$givi = new Givi();
524$givi->main($argv);