diff --git a/scripts/update_cli.php b/scripts/update_cli.php index e3780f7c1..8cc7567e5 100644 --- a/scripts/update_cli.php +++ b/scripts/update_cli.php @@ -28,10 +28,12 @@ function printHelp() { echo "Usage: php update_cli.php [options]\n\n"; echo "Options:\n"; echo " --help Show this help message.\n"; - echo " --update Perform a git pull to update the application.\n"; - echo " --force_update Perform a git fetch and hard reset to the branch this install tracks.\n"; - echo " --update_db Update the database structure to the latest version.\n"; - echo "\nIf no options are provided, a standard update (git pull) is performed.\n"; + echo " --update Update the application only (git pull).\n"; + echo " --force_update Update the application only, discarding local changes: git fetch\n"; + echo " and hard reset to the branch this install tracks.\n"; + echo " --update_db Update the database structure only.\n"; + echo "\nWith no options a full update is performed - the application first, then the\n"; + echo "database. That is the same as running --update --update_db.\n"; } // Define allowed options (removed 'user') @@ -74,46 +76,98 @@ if (isset($options['help'])) { exit; } -// If no recognized options are passed, default to --update +// If no recognized options are passed, update everything - the application and then the +// database. The switches stay surgical for anyone who wants one or the other, which is +// what --update_db is for after updating the files from the web UI. if (count($options) === 0) { $options['update'] = true; + $options['update_db'] = true; } +// Whether the working tree actually moved. Decides how the database phase runs below +$app_updated = false; + // If "update" or "force_update" is requested if (isset($options['update']) || isset($options['force_update'])) { + + // Recorded either side of the update so that "did anything change" is a commit + // comparison rather than a match on git's wording, which varies by version and locale + $head_before = trim((string) exec("git rev-parse HEAD 2>/dev/null")); + + $output = []; + if (isset($options['force_update'])) { // Perform a hard reset onto the tracked branch. This named origin/master outright // until 26.08, which moved any install tracking another branch onto master. $remote_ref = escapeshellarg("origin/" . getRepoBranch()); exec("git fetch --all 2>&1", $output, $return_var); - exec("git reset --hard $remote_ref 2>&1", $output2, $return_var2); - echo implode("\n", $output) . "\n" . implode("\n", $output2) . "\n"; + if ($return_var === 0) { + exec("git reset --hard $remote_ref 2>&1", $output, $return_var); + } } else { // Perform a standard update (git pull) exec("git pull 2>&1", $output, $return_var); - - // Check if the repository is already up to date - if (strpos(implode("\n", $output), 'Already up to date.') === false) { - echo implode("\n", $output) . "\n"; - echo "Update successful\n"; - } else { - // If already up-to-date, don't show the update success message - echo implode("\n", $output) . "\n"; + } + + echo implode("\n", $output) . "\n"; + + // git reports merge conflicts, a dirty working tree and network failures through its + // exit status. Without this the script printed the error, said "Update successful" + // and exited 0 - and would now go on to update the database as if all was well + if ($return_var !== 0) { + fwrite(STDERR, "Error: the application update failed - see the output above.\n"); + if (isset($options['update_db'])) { + fwrite(STDERR, "The database has been left alone. Fix the problem and run this script again, or run\nphp update_cli.php --update_db on its own to update only the database.\n"); } + exit(1); + } + + $head_after = trim((string) exec("git rev-parse HEAD 2>/dev/null")); + $app_updated = $head_before !== '' && $head_after !== '' && $head_before !== $head_after; + + if ($app_updated) { + echo "Update successful\n"; } } // If "update_db" is requested if (isset($options['update_db'])) { + + /* + * If the application was just updated, this process is still running the code it + * started with - config.php, functions.php and everything functions.php loaded are + * the pre-update copies - while the migration runner and the migration files + * themselves get read fresh off the new tree. A migration calling a helper added in + * the same release then dies with "Call to undefined function". + * + * So hand the database phase to a new process, which loads the new code from the + * start. Its output and its exit status are this script's. A process that did not + * update anything is already running the code on disk and needs none of this. + */ + if ($app_updated && PHP_BINARY !== '' && function_exists('passthru')) { + echo "Running the database update against the updated code\n"; + passthru(escapeshellarg(PHP_BINARY) . " " . escapeshellarg(__FILE__) . " --update_db", $db_return_var); + exit($db_return_var); + } + require_once "../includes/database_version.php"; $latest_db_version = LATEST_DATABASE_VERSION; // Fetch the current version from the database $result = mysqli_query($mysqli, "SELECT config_current_database_version FROM settings LIMIT 1"); - $row = mysqli_fetch_assoc($result); - DEFINE("CURRENT_DATABASE_VERSION", $row['config_current_database_version']); - $old_db_version = $row['config_current_database_version']; + $row = $result ? mysqli_fetch_assoc($result) : null; + $old_db_version = trim((string) ($row['config_current_database_version'] ?? '')); + + // No version to work from means no way to tell which migrations have already run, and + // starting from the first one against a partly built database breaks it + if ($old_db_version === '') { + fwrite(STDERR, "Error: could not read the current database version from the settings table.\n"); + fwrite(STDERR, "This install looks incomplete - finish it at /setup before updating the database.\n"); + exit(1); + } + + DEFINE("CURRENT_DATABASE_VERSION", $old_db_version); // Run the migrations - populates $database_updates_applied and $database_updates_error require_once "../admin/database_updates.php";