#!/usr/bin/env php getMessage(), $e->getFile(), $e->getLine() )); exit(1); }); $options = parseArguments($argv); if ($options['help']) { printHelp(); exit(0); } $project = $options['project']; if ($project === null) { fwrite(STDERR, "Es fehlt --project=. Eine Roadmap gehoert zu genau einem Projekt.\n"); exit(1); } $file = $options['file'] ?? (DC_ROOT . '/docs/ROADMAP.md'); $db = Db::init(); $roadmap = new Roadmap($db); $mode = $options['mode']; // --------------------------------------------------------------------------- // --list: nur ansehen, nichts anfassen // --------------------------------------------------------------------------- if ($mode === 'list') { $stats = $roadmap->stats($project); if ($stats['total'] === 0) { fwrite(STDOUT, sprintf("Projekt \"%s\" hat keine Roadmap-Eintraege.\n", $project)); exit(2); } fwrite(STDOUT, sprintf( "%s — %d Vorhaben: %d offen, %d in Arbeit, %d erledigt, %d zurueckgestellt, %d verworfen\n\n", $project, $stats['total'], $stats['pending'], $stats['working'], $stats['done'], $stats['deferred'], $stats['rejected'] )); foreach ($roadmap->epics($project) as $epic) { fwrite(STDOUT, sprintf( " [%s] %-40s %d Eintrag/Eintraege\n", (string)$epic['epic'], (string)$epic['title'], (int)$epic['total'] )); } fwrite(STDOUT, "\nAls Naechstes:\n"); foreach ($roadmap->nextUp($project, 5) as $i => $item) { fwrite(STDOUT, sprintf( " %d. [%s] #%d %s%s\n", $i + 1, (string)$item['epic'], (int)$item['id'], (string)$item['title'], $item['claimed_by'] !== null ? ' (in Arbeit: ' . $item['claimed_by'] . ')' : '' )); } exit(0); } // --------------------------------------------------------------------------- // --write: Zustandsblock erzeugen und einsetzen // --------------------------------------------------------------------------- if ($mode === 'write') { $block = $roadmap->renderBlock($project); if ($options['stdout']) { fwrite(STDOUT, $block . "\n"); exit(0); } $existing = is_file($file) ? (string)file_get_contents($file) : ''; $updated = Roadmap::spliceBlock($existing, $block); if ($existing === $updated) { fwrite(STDOUT, sprintf("%s ist bereits auf dem Stand.\n", $file)); exit(0); } // Erst schreiben, dann melden. Ein fehlgeschlagenes file_put_contents // duerfte nicht als Erfolg durchgehen - sonst haelt der naechste Lauf die // Datei fuer aktuell. if (file_put_contents($file, $updated) === false) { fwrite(STDERR, sprintf("Konnte %s nicht schreiben.\n", $file)); exit(1); } $wasNew = strpos($existing, Roadmap::BLOCK_START) === false; fwrite(STDOUT, sprintf( "%s %s.\n", $file, $wasNew ? 'ergaenzt - der erzeugte Block wurde angehaengt' : 'aktualisiert' )); if ($wasNew && $existing !== '') { fwrite(STDOUT, "Hinweis: Der Block steht am Dateiende. Verschiebe ihn samt Marker an die\n" . " gewuenschte Stelle - kuenftige Laeufe finden ihn dort wieder.\n" ); } exit(0); } // --------------------------------------------------------------------------- // --check: Prosa gegen die Items // --------------------------------------------------------------------------- $result = $roadmap->check($project, $file); if ($options['json']) { fwrite(STDOUT, json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"); exit($result['ok'] ? 0 : 1); } fwrite(STDOUT, sprintf("Abgleich %s gegen Projekt \"%s\"\n", $file, $project)); fwrite(STDOUT, sprintf("%d Item-Verweis(e) in der Prosa geprueft.\n", $result['checked'])); // Ein veralteter Block ist kein Fehler - er ist eine Kopie, und Kopien altern. // Gemeldet wird er trotzdem, damit niemand eine Woche alte Liste fuer den // Stand von heute haelt. if ($result['stale'] !== null) { $age = time() - (int)strtotime($result['stale']); if ($age > 86400) { fwrite(STDOUT, sprintf( "\n[Hinweis] Der erzeugte Block ist vom %s, also %d Tag(e) alt.\n" . " `--write` bringt ihn auf den Stand. Der Live-Stand steht im DC.\n", $result['stale'], (int)floor($age / 86400) )); } } else { fwrite(STDOUT, "\n[Hinweis] Die Datei enthaelt keinen erzeugten Block. `--write` legt ihn an;\n" . " bis dahin steht der Zustand nur im Deploymentcenter.\n" ); } if ($result['findings'] === []) { fwrite(STDOUT, "\nKeine Abweichungen.\n"); exit(0); } $errors = 0; $warnings = 0; fwrite(STDOUT, "\n"); foreach ($result['findings'] as $finding) { $isError = $finding['level'] === 'error'; $isError ? $errors++ : $warnings++; fwrite($isError ? STDERR : STDOUT, sprintf( " %s [%s] %s\n", $isError ? 'FEHLER ' : 'Warnung', $finding['code'], $finding['message'] )); } fwrite(STDOUT, sprintf("\n%d Fehler, %d Warnung(en).\n", $errors, $warnings)); exit($errors > 0 ? 1 : 0); // =========================================================================== // Hilfsfunktionen // =========================================================================== /** * @param list $argv * @return array{mode:string,project:?string,file:?string,json:bool,stdout:bool,help:bool} */ function parseArguments(array $argv): array { $options = [ 'mode' => 'check', 'project' => null, 'file' => null, 'json' => false, 'stdout' => false, 'help' => false, ]; foreach (array_slice($argv, 1) as $arg) { if ($arg === '--help' || $arg === '-h') { $options['help'] = true; } elseif ($arg === '--check') { $options['mode'] = 'check'; } elseif ($arg === '--write') { $options['mode'] = 'write'; } elseif ($arg === '--list') { $options['mode'] = 'list'; } elseif ($arg === '--json') { $options['json'] = true; } elseif ($arg === '--stdout') { $options['stdout'] = true; } elseif (str_starts_with($arg, '--project=')) { $options['project'] = substr($arg, 10); } elseif (str_starts_with($arg, '--file=')) { $options['file'] = substr($arg, 7); } else { fwrite(STDERR, sprintf("Unbekannte Option \"%s\". --help zeigt die Aufrufe.\n", $arg)); exit(1); } } return $options; } function printHelp(): void { fwrite(STDOUT, <<<'TEXT' Roadmap — Zustandsblock erzeugen und Prosa abgleichen. php cli/roadmap.php --check --project= [--file=] [--json] php cli/roadmap.php --write --project= [--file=] [--stdout] php cli/roadmap.php --list --project= Was wo gepflegt wird: Zustand Status, Reihenfolge, Zielfassung, Bearbeiter -> Wahrheit ist das Deploymentcenter. Die Datei zeigt eine erzeugte Kopie zwischen den Markern. Begruendung Warum, verworfen, Ausloeser, Herleitung -> Wahrheit ist die Datei, ausserhalb der Marker. Das Item haelt nur den Verweis darauf (doc_anchor). Kein Feld hat zwei Pfleger. Deshalb kann der Zustandsteil nicht driften - und deshalb prueft --check nur die Prosa. Rueckgabewerte: 0 in Ordnung, 1 Fehler, 2 nichts zu tun. TEXT); }