src/shipit/phase/ShipItPhaseRunner.php (232 lines of code) (raw):

<?hh /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * @format */ /** * This file was moved from fbsource to www. View old history in diffusion: * https://fburl.com/8yredn7r */ namespace Facebook\ShipIt; use namespace HH\Lib\{C, Dict, Math, Str, Vec}; // @oss-enable class ShipItPhaseRunner { protected IShipItArgumentParser $argumentParser; public function __construct( protected ShipItManifest $manifest, protected vec<ShipItPhase> $phases, ?IShipItArgumentParser $argumentParser = null, ) { $this->argumentParser = $argumentParser ?? new ShipItCLIArgumentParser(); } public async function genRun(): Awaitable<void> { await $this->genParseCLIArguments(); try { foreach ($this->phases as $phase) { // @lint-ignore AWAIT_IN_LOOP need sync execution await $phase->genRun($this->manifest); } } finally { if ($this->manifest->hasSourceSharedLock()) { $this->manifest->getSourceSharedLock()->release(); } if ($this->manifest->hasDestinationSharedLock()) { $this->manifest->getDestinationSharedLock()->release(); } } } protected function getBasicCLIArguments(): vec<ShipItCLIArgument> { return vec[ shape( 'short_name' => 'h', 'long_name' => 'help', 'description' => 'show this help message and exit', ), shape( 'long_name' => 'base-dir::', 'description' => 'Path to store repositories', 'write' => $x ==> { $this->manifest = $this->manifest ->withBaseDirectory(Str\trim($x)); return $this->manifest; }, ), shape( 'long_name' => 'source-repo-dir::', 'description' => 'path to fetch source from', 'write' => $x ==> { $this->manifest = $this->manifest ->withSourcePath(Str\trim($x)); return $this->manifest; }, ), shape( 'long_name' => 'destination-repo-dir::', 'description' => 'path to push filtered changes to', 'write' => $x ==> { $this->manifest = $this->manifest ->withDestinationPath(Str\trim($x)); return $this->manifest; }, ), shape( 'long_name' => 'source-branch::', 'description' => "Branch to sync from", 'write' => $x ==> { $this->manifest = $this->manifest ->withSourceBranch(Str\trim($x)); return $this->manifest; }, ), shape( 'long_name' => 'destination-branch::', 'description' => 'Branch to sync to', 'write' => $x ==> { $this->manifest = $this->manifest ->withDestinationBranch(Str\trim($x)); return $this->manifest; }, ), shape( 'short_name' => 'v', 'long_name' => 'verbose', 'description' => 'Give more verbose output', 'write' => $_ ==> { $this->manifest = $this->manifest->withVerboseEnabled(); return $this->manifest; }, ), ]; } final public function getCLIArguments(): vec<ShipItCLIArgument> { $args = $this->getBasicCLIArguments(); foreach ($this->phases as $phase) { $args = Vec\concat($args, $phase->getCLIArguments()); } // Check for correctness foreach ($args as $arg) { $description = Shapes::idx($arg, 'description'); $replacement = Shapes::idx($arg, 'replacement'); $handler = Shapes::idx($arg, 'write'); $name = $arg['long_name']; invariant( !($description !== null && $replacement !== null), '--%s is documented and deprecated', $name, ); invariant( !( $handler !== null && !($description !== null || $replacement !== null) ), '--%s does something, and is undocumented', $name, ); } return $args; } final protected function parseOptions( vec<ShipItCLIArgument> $config, dict<string, mixed> $raw_opts, ): void { foreach ($config as $opt) { $is_optional = Str\slice($opt['long_name'], -2) === '::'; $is_required = !$is_optional && Str\slice($opt['long_name'], -1) === ':'; $is_bool = !$is_optional && !$is_required; $short = Str\trim_right(Shapes::idx($opt, 'short_name', ''), ':'); $long = Str\trim_right($opt['long_name'], ':'); if (!Str\is_empty($short) && C\contains_key($raw_opts, $short)) { $key = '-'.$short; $value = $is_bool ? true : $raw_opts[$short]; } else if (C\contains_key($raw_opts, $long)) { $key = '--'.$long; $value = $is_bool ? true : $raw_opts[$long]; } else { $key = null; $value = $is_bool ? false : ''; if ($is_required) { ShipItLogger::err("ERROR: Expected --%s\n\n", $long); self::printHelp($config); throw new ShipItExitException(1); } } $handler = Shapes::idx($opt, 'write'); if ($handler && $value !== '' && $value !== false) { $handler((string)$value); } if ($key === null) { continue; } $description = Shapes::idx($opt, 'description'); if ($description !== null && $description !== '') { continue; } $replacement = Shapes::idx($opt, 'replacement'); if ($replacement !== null) { ShipItLogger::err( "%s %s, use %s instead\n", $key, $handler ? 'is deprecated' : 'has been removed', $replacement, ); if ($handler === null) { throw new ShipItExitException(1); } } else { invariant( $handler === null, "Option '%s' is not a no-op, is undocumented, and doesn't have a ". 'documented replacement.', $key, ); ShipItLogger::err("%s is deprecated and a no-op\n", $key); } } } protected async function genParseCLIArguments(): Awaitable<void> { $config = $this->getCLIArguments(); $raw_opts = $this->argumentParser->parseArgs($config); if (C\contains_key($raw_opts, 'h') || C\contains_key($raw_opts, 'help')) { self::printHelp($config); throw new ShipItExitException(0); } $this->parseOptions($config, $raw_opts); } protected static function printHelp(vec<ShipItCLIArgument> $config): void { /* HH_FIXME[2050] Previously hidden by unsafe_expr */ $filename = $_SERVER['SCRIPT_NAME']; $max_left = 0; $rows = dict[]; foreach ($config as $opt) { $description = Shapes::idx($opt, 'description'); if ($description === null) { $replacement = Shapes::idx($opt, 'replacement'); if ($replacement !== null) { continue; } else { invariant( !Shapes::idx($opt, 'write'), '--%s is undocumented, does something, and has no replacement', $opt['long_name'], ); $description = 'deprecated, no-op'; } } $short = Shapes::idx($opt, 'short_name'); $long = $opt['long_name']; $is_optional = Str\slice($long, -2) === '::'; $is_required = !$is_optional && Str\slice($long, -1) === ':'; $long = Str\trim_right($long, ':'); $prefix = $short !== null ? '-'.Str\trim_right($short, ':').', ' : ''; $suffix = $is_optional ? "=VALUE" : ($is_required ? "=$long" : ''); $left = ' '.$prefix.'--'.$long.$suffix; $max_left = Math\maxva(Str\length($left), $max_left); $rows[$long] = tuple($left, $description); } $rows = Dict\sort_by_key($rows); $help = $rows['help']; unset($rows['help']); $rows = Dict\merge(dict['help' => $help], $rows); $opt_help = Str\join( Dict\map( $rows, $row ==> Str\format("%s %s\n", Str\pad_right($row[0], $max_left), $row[1]), ), "", ); echo <<<EOF Usage: {$filename} [options] Options: {$opt_help} EOF; } }