Hoa central
Public Member Functions | Protected Attributes | List of all members
Hoa\Devtools\Bin\Snapshot Class Reference
Inheritance diagram for Hoa\Devtools\Bin\Snapshot:

Public Member Functions

 main ()
 
 usage ()
 
- Public Member Functions inherited from Hoa\Console\Dispatcher\Kit
 __construct (Router $router, Dispatcher $dispatcher, View\Viewable $view=null)
 
 getOption (&$optionValue, $short=null)
 
 setOptions (Array $options)
 
 makeUsageOptionsList (Array $definitions=[])
 
 resolveOptionAmbiguity (Array $solutions)
 
 status ($text, $status)
 
 readLine ($prefix=null)
 
 readPassword ($prefix=null)
 
- Public Member Functions inherited from Hoa\Dispatcher\Kit
 __construct (Router $router, Dispatcher $dispatcher, View\Viewable $view=null)
 
 construct ()
 

Protected Attributes

 $options
 
- Protected Attributes inherited from Hoa\Console\Dispatcher\Kit
 $options = null
 
 $_options = null
 

Additional Inherited Members

- Public Attributes inherited from Hoa\Console\Dispatcher\Kit
 $parser = null
 
- Public Attributes inherited from Hoa\Dispatcher\Kit
 $router = null
 
 $dispatcher = null
 
 $view = null
 
 $data = null
 

Detailed Description

Class .

Assistant to create a snapshot.

Definition at line 50 of file Snapshot.php.

Member Function Documentation

Hoa\Devtools\Bin\Snapshot::main ( )

The entry method.

Returns
int

Definition at line 74 of file Snapshot.php.

75  {
76  $breakBC = false;
77  $minimumTag = null;
78  $doSteps = [
79  // -1 and 1 mean true,
80  // 0 means false.
81  'test' => -1,
82  'changelog' => -1,
83  'tag' => -1,
84  'github' => -1
85  ];
86 
87  $onlyStep = function ($step) use (&$doSteps) {
88  $doSteps[$step] = 1;
89 
90  foreach ($doSteps as &$doStep) {
91  if (-1 === $doStep) {
92  $doStep = 0;
93  }
94  }
95 
96  return;
97  };
98 
99  while (false !== $c = $this->getOption($v)) {
100  switch ($c) {
101  case '__ambiguous':
102  $this->resolveOptionAmbiguity($v);
103 
104  break;
105 
106  case 'c':
107  $onlyStep('changelog');
108 
109  break;
110 
111  case 't':
112  $onlyStep('tag');
113 
114  break;
115 
116  case 'g':
117  $onlyStep('github');
118 
119  break;
120 
121  case 'b':
122  $breakBC = $v;
123 
124  break;
125 
126  case 'm':
127  $minimumTag = $v;
128 
129  break;
130 
131  case 'h':
132  case '?':
133  default:
134  return $this->usage();
135  }
136  }
137 
138  $this->parser->listInputs($repositoryRoot);
139 
140  if (empty($repositoryRoot)) {
141  return $this->usage();
142  }
143 
144  if (false === file_exists($repositoryRoot . DS . '.git')) {
145  throw new Console\Exception(
146  '%s is not a valid Git repository.',
147  0, $repositoryRoot);
148  }
149 
150  date_default_timezone_set('UTC');
151 
152  $allTags = $tags = explode(
153  "\n",
154  Console\Processus::execute(
155  'git --git-dir=' . $repositoryRoot . '/.git ' .
156  'tag'
157  )
158  );
159  rsort($tags);
160 
161  list($currentMCN) = explode('.', $tags[0], 2);
162 
163  if (true === $breakBC) {
164  ++$currentMCN;
165  }
166 
167  $newTag = $currentMCN . '.' . date('y.m.d');
168 
169  if (null === $minimumTag) {
170  $tags = [$tags[0]];
171  } else {
172  $toInt = function ($tag) {
173  list($x, $y, $m, $d) = explode('.', $tag);
174 
175  return $x * 1000000 + $y * 10000 + $m * 100 + $d * 1;
176  };
177 
178  $_tags = [];
179  $_minimumTag = $toInt($minimumTag);
180 
181  foreach ($tags as $tag) {
182  if ($toInt($tag) >= $_minimumTag) {
183  $_tags[] = $tag;
184  }
185  }
186 
187  $tags = $_tags;
188  }
189 
190  $changelog = '';
191 
192  echo 'We are going to snapshot this library together, by following ',
193  'these steps:', "\n",
194  ' 1. tests must pass,', "\n",
195  ' 2. updating the CHANGELOG.md file,', "\n",
196  ' 3. commit the CHANGELOG.md file,', "\n",
197  ' 4. creating a tag,', "\n",
198  ' 5. pushing the tag,', "\n",
199  ' 6. creating a release on Github.', "\n";
200 
201  $step = function ($stepGroup, $message, $task) use ($doSteps) {
202  echo "\n\n";
203  Console\Cursor::colorize('foreground(black) background(yellow)');
204  echo 'Step “', $message, '”.';
205  Console\Cursor::colorize('normal');
206  echo "\n";
207 
208  if (0 === $doSteps[$stepGroup]) {
209  $answer = 'no';
210  } else {
211  $answer = $this->readLine('Would you like to do this one: [yes/no] ');
212  }
213 
214  if ('yes' === $answer) {
215  echo "\n";
216  $task();
217  } else {
218  Console\Cursor::colorize('foreground(red)');
219  echo 'Aborted!', "\n";
220  Console\Cursor::colorize('normal');
221  }
222  };
223 
224  $step(
225  'test',
226  'tests must pass',
227  function () {
228  echo
229  'Tests must be green. Execute:', "\n",
230  ' $ hoa test:run -d Test', "\n",
231  'to run the tests.', "\n";
232 
233  $this->readLine('Press Enter when it is green (or Ctrl-C to abort).');
234  }
235  );
236 
237  $step(
238  'changelog',
239  'updating the CHANGELOG.md file',
240  function () use ($tags, $newTag, $repositoryRoot, &$changelog) {
241  array_unshift($tags, 'HEAD');
242 
243  $changelog = null;
244 
245  for ($i = 0, $max = count($tags) - 1; $i < $max; ++$i) {
246  $fromStep = $tags[$i];
247  $toStep = $tags[$i + 1];
248  $title = $fromStep;
249 
250  if ('HEAD' === $fromStep) {
251  $title = $newTag;
252  }
253 
254  $changelog .=
255  '# ' . $title . "\n\n" .
257  'git --git-dir=' . $repositoryRoot . '/.git ' .
258  'log ' .
259  '--first-parent ' .
260  '--pretty="format: * %s (%aN, %aI)" ' .
261  $fromStep . '...' . $toStep,
262  false
263  ) . "\n\n";
264  }
265 
266  $file = new File\ReadWrite($repositoryRoot . DS . 'CHANGELOG.md');
267  $file->rewind();
268 
269  $temporary = new File\ReadWrite($repositoryRoot . DS . '._hoa.CHANGELOG.md');
270  $temporary->truncate(0);
271  $temporary->writeAll($changelog);
272  $temporary->close();
273 
274  echo 'The CHANGELOG is ready.', "\n";
275  $this->readLine('Press Enter to check and edit the file (empty the file to abort).');
276 
277  Console\Chrome\Editor::open($temporary->getStreamName());
278 
279  $temporary->open();
280  $changelog = $temporary->readAll();
281 
282  if (empty(trim($changelog))) {
283  $temporary->delete();
284  $temporary->close();
285 
286  exit;
287  }
288 
289  $previous = $file->readAll();
290  $file->truncate(0);
291  $file->writeAll($changelog . $previous);
292 
293  $temporary->delete();
294  $temporary->close();
295  $file->close();
296 
297  return;
298  }
299  );
300 
301  $step(
302  'changelog',
303  'commit the CHANGELOG.md file',
304  function () use ($newTag, $repositoryRoot) {
306  'git --git-dir=' . $repositoryRoot . '/.git ' .
307  'add ' .
308  '--verbose ' .
309  'CHANGELOG.md'
310  );
312  'git --git-dir=' . $repositoryRoot . '/.git ' .
313  'commit ' .
314  '--verbose ' .
315  '--message "Prepare ' . $newTag . '." ' .
316  'CHANGELOG.md'
317  );
318 
319  return;
320  }
321  );
322 
323  $step(
324  'tag',
325  'creating a tag',
326  function () use (
327  $breakBC,
328  $step,
329  $currentMCN,
330  $repositoryRoot,
331  $tags,
332  $newTag,
333  $allTags
334  ) {
335  if (true === $breakBC) {
336  echo 'A BC break has been introduced, ',
337  'few more steps are required:', "\n";
338 
339  $step(
340  'tag',
341  'update the composer.json file',
342  function () use ($currentMCN) {
343  echo 'The `extra.branch-alias.dev-master` value ',
344  'must be set to `',
345  $currentMCN, '.x-dev`', "\n";
346 
347  $this->readLine('Press Enter to edit the file.');
348 
350  $repository . DS . 'composer.json'
351  );
352  }
353  );
354 
355  $step(
356  'tag',
357  'open issues to update parent dependencies',
358  function () {
359  echo 'Some libraries may depend on this one. ',
360  'Issues must be opened to update this ',
361  'dependency.', "\n";
362 
363  $this->readLine('Press Enter when it is done (or Ctrl-C to abort).');
364  }
365  );
366 
367  $step(
368  'tag',
369  'update the README.md file',
370  function () use ($currentMCN) {
371  echo 'The installation Section must invite the ',
372  'user to install the version ',
373  '`~', $currentMCN, '.0`.', "\n";
374 
375  $this->readLine('Press Enter when it is done (or Ctrl-C to abort).');
376 
378  $repository . DS . 'README.md'
379  );
380  }
381  );
382 
383  $step(
384  'tag',
385  'commit the composer.json and README.md files',
386  function () use ($currentMCN) {
388  'git --git-dir=' . $repositoryRoot . '/.git ' .
389  'add ' .
390  '--verbose ' .
391  'composer.json README.md'
392  );
394  'git --git-dir=' . $repositoryRoot . '/.git ' .
395  'commit ' .
396  '--verbose ' .
397  '--message "Update because of the BC break." ' .
398  'composer.json README.md'
399  );
400  }
401  );
402  }
403 
404  $status = Console\Processus::execute(
405  'git --git-dir=' . $repositoryRoot . '/.git ' .
406  'status ' .
407  '--short'
408  );
409 
410  if (!empty($status)) {
411  Console\Cursor::colorize('foreground(white) background(red)');
412  echo 'At least one file is not commited!';
413  Console\Cursor::colorize('normal');
414  echo
415  "\n", '(tips: use `git stash` if it is not related ',
416  'to this snapshot)', "\n";
417 
418  $this->readLine('Press Enter when everything is clean.');
419  }
420 
421  echo
422  'Here is the list of tags:', "\n",
423  ' * ', implode(',' . "\n" . ' * ', $allTags), '.', "\n",
424  'We are going to create the following tag: ',
425  $newTag, '.', "\n";
426 
427  $answer = $this->readLine('Is it correct? [yes/no] ');
428 
429  if ('yes' !== $answer) {
430  return;
431  }
432 
434  'git --git-dir=' . $repositoryRoot . '/.git ' .
435  'tag ' . $newTag
436  );
437  }
438  );
439 
440  $step(
441  'tag',
442  'push the new snapshot',
443  function () use ($repositoryRoot) {
444  Console\Cursor::colorize('foreground(white) background(red)');
445  echo 'This step ',
446  Console\Cursor::colorize('underlined');
447  echo 'must not';
448  Console\Cursor::colorize('!underlined');
449  echo ' be undo!';
450  Console\Cursor::colorize('normal');
451 
452  echo "\n";
453 
454  $i = 5;
455  while ($i-- > 0) {
456  Console\Cursor::clear('↔');
457  echo($i + 1);
458  sleep(1);
459  }
460 
461  $remotes = Console\Processus::execute(
462  'git --git-dir=' . $repositoryRoot . '/.git ' .
463  'remote ' .
464  '--verbose'
465  );
466 
467  $gotcha = false;
468 
469  foreach (explode("\n", $remotes) as $remote) {
470  if (0 !== preg_match('/(git@git.hoa-project.net:[^ ]+)/', $remote, $matches)) {
471  $gotcha = true;
472 
473  break;
474  }
475  }
476 
477  if (false === $gotcha) {
478  echo 'No remote has been found.';
479 
480  return;
481  }
482 
483  echo
484  "\n", 'To push tag, execute:', "\n",
485  ' $ git push ', $matches[1], "\n",
486  ' $ git push ', $matches[1], ' --tags', "\n";
487 
488  $this->readLine('Press Enter when it is done (or Ctrl-C to abort).');
489  }
490  );
491 
492  $step(
493  'github',
494  'create a release on Github',
495  function () use ($newTag, $changelog, $repositoryRoot) {
496  $temporary = new File\ReadWrite($repositoryRoot . DS . '._hoa.GithubRelease.md');
497  $temporary->truncate(0);
498 
499  if (!empty($changelog)) {
500  $temporary->writeAll($changelog);
501  }
502 
503  $temporary->close();
504 
505  Console\Chrome\Editor::open($temporary->getStreamName());
506 
507  $temporary->open();
508  $temporary->rewind();
509  $body = $temporary->readAll();
510  $temporary->delete();
511 
512  $composer = json_decode(file_get_contents('composer.json'));
513  list(, $libraryName) = explode('/', $composer->name);
514 
515  $output = json_encode([
516  'tag_name' => $newTag,
517  'body' => $body
518  ]);
519 
520  $username = $this->readLine('Username: ');
521  $password = $this->readPassword('Password: ');
522  $auth = base64_encode($username . ':' . $password);
523 
524  $context = stream_context_create([
525  'http' => [
526  'method' => 'POST',
527  'header' => 'Host: api.github.com' . CRLF .
528  'User-Agent: Hoa\Devtools' . CRLF .
529  'Accept: application/json' . CRLF .
530  'Content-Type: application/json' . CRLF .
531  'Content-Length: ' . strlen($output) . CRLF .
532  'Authorization: Basic ' . $auth . CRLF,
533  'content' => $output
534  ]
535  ]);
536 
537  echo file_get_contents(
538  'https://api.github.com/repos/hoaproject/' . $libraryName . '/releases',
539  false,
540  $context
541  );
542  }
543  );
544 
545  echo "\n", '🍺 🍺 🍺', "\n";
546 
547  return;
548  }
$composer
readPassword($prefix=null)
Definition: Kit.php:260
static open($file= '', $editor=null)
Definition: Editor.php:59
static execute($commandLine, $escape=true)
Definition: Processus.php:1165
static colorize($attributes)
Definition: Cursor.php:379
static clear($parts= 'all')
Definition: Cursor.php:271
getOption(&$optionValue, $short=null)
Definition: Kit.php:104
resolveOptionAmbiguity(Array $solutions)
Definition: Kit.php:190
readLine($prefix=null)
Definition: Kit.php:243

Here is the call graph for this function:

Hoa\Devtools\Bin\Snapshot::usage ( )

The command usage.

Returns
int

Definition at line 555 of file Snapshot.php.

556  {
557  echo
558  'Usage : devtools:snapshot <options> repository-root', "\n",
559  'Options :', "\n",
560  $this->makeUsageOptionsList([
561  'b' => 'Whether we have break the backward compatibility ' .
562  'or not.',
563  'c' => 'Only do steps related to the CHANGELOG.md file.',
564  't' => 'Only do steps related to the tag.',
565  'g' => 'Only do steps related to Github release.',
566  'm' => 'Set the minimum tag (default: the latest, often ' .
567  'useful with --only-changelog).',
568  'help' => 'This help.'
569  ]), "\n";
570 
571  return;
572  }
makeUsageOptionsList(Array $definitions=[])
Definition: Kit.php:149

Here is the call graph for this function:

Here is the caller graph for this function:

Member Data Documentation

Hoa\Devtools\Bin\Snapshot::$options
protected
Initial value:
= [
['only-changelog', Console\GetOption::NO_ARGUMENT, 'c'],
['only-tag', Console\GetOption::NO_ARGUMENT, 't'],
['only-github-release', Console\GetOption::NO_ARGUMENT, 'g'],
['break-bc', Console\GetOption::NO_ARGUMENT, 'b'],
['minimum-tag', Console\GetOption::REQUIRED_ARGUMENT, 'm'],
]

Definition at line 57 of file Snapshot.php.


The documentation for this class was generated from the following file: