Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/docs/user/configuration/storage_adjust.diviner b/src/docs/user/configuration/storage_adjust.diviner
index 468a82697b..2403cd3fed 100644
--- a/src/docs/user/configuration/storage_adjust.diviner
+++ b/src/docs/user/configuration/storage_adjust.diviner
@@ -1,147 +1,191 @@
@title Managing Storage Adjustments
@group config
Explains how to apply storage adjustments to the MySQL schemata.
Overview
========
Phabricator uses a workflow called //storage adjustment// to make some minor
kinds of changes to the MySQL schema. This workflow compliments the //storage
upgrade// workflow, which makes major changes.
You can perform storage adjustment by running:
phabricator/ $ ./bin/storage adjust
This document describes what adjustments are, how they relate to storage
upgrades, how to perform them, and how to troubleshoot issues with storage
adjustment.
Understanding Adjustments
===================
Storage adjustments make minor changes to the Phabricator MySQL schemata to
improve consistency, unicode handling, and performance. Changes covered by
adjustment include:
- Character set and collation settings for columns, tables, and databases.
- Setting and removing "Auto Increment" on columns.
- Adding, removing, renaming and adjusting keys.
Adjustment does not make major changes to the schemata, like creating or
removing columns or tables or migrating data. (Major changes are performed by
the upgrade workflow.)
Adjustments are separate from upgrades primarily because adjustments depend on
the MySQL version, while upgrades do not. If you update MySQL, better collations
may become available, and the adjustment workflow will convert your schemata to
use them.
All changes covered by adjustment are minor, and technically optional. However,
you are strongly encouraged to apply outstanding adjustments: if you do not,
you may encounter issues storing or sorting some unicode data, and may suffer
poor performance on some queries.
Reviewing Outstanding Adjustments
=================================
There are two ways to review outstanding adjustments: you can use the web UI,
or you can use the CLI.
To access the web UI, navigate to {nav Config > Database Status} or
{nav Config > Database Issues}. The //Database Status// panel provides a general
overview of all schemata. The //Database Issues// panel shows outstanding
issues.
These interfaces report //Errors//, which are serious issues that can not be
resolved through adjustment, and //Warnings//, which are minor issues that the
adjustment workflow can resolve.
You can also review adjustments from the CLI, by running:
phabricator/ $ ./bin/storage adjust
Before you're prompted to actually apply adjustments, you'll be given a list of
available adjustments. You can then make a choice to apply them.
Performing Adjustments
======================
To perform adjustments, run the `adjust` workflow:
phabricator/ $ ./bin/storage adjust
For details about flags, use:
phabricator/ $ ./bin/storage help adjust
You do not normally need to run this workflow manually: it will be run
automatically after you run the `upgrade` workflow.
History and Rationale
=====================
The primary motivation for the adjustment workflow is MySQL's handling of
unicode character sets. Before MySQL 5.5, MySQL supports a character set called
`utf8`. However, this character set can not store 4-byte unicode characters
(including emoji). Inserting 4-byte characters into a `utf8` column truncates
the data.
With MySQL 5.5, a new `utf8mb4` character set was introduced. This character
set can safely store 4-byte unicode characters.
The adjustment workflow allows us to alter the schema to primarily use
`binary` character sets on older MySQL, and primarily use `utf8mb4` character
sets on newer MySQL. The net effect is that Phabricator works consistently and
can store 4-byte unicode characters regardless of the MySQL version. Under
newer MySQL, we can also take advantage of the better collation rules the
`utf8mb4` character set offers.
The adjustment workflow was introduced in November 2014. If your install
predates its introduction, your first adjustment may take a long time (we must
convert all of the data out of `utf8` and into the appropriate character set).
If your install was set up after November 2014, adjustments should generally
be very minor and complete quickly, unless you perform a major MySQL update and
make new character sets available.
If you plan to update MySQL from an older version to 5.5 or newer, it is
advisable to update first, then run the adjustment workflow. If you adjust
first, you'll need to adjust again after updating, so you'll end up spending
twice as much time performing schemata adjustments.
Troubleshooting
===============
-When you apply adjustments, some adjustments may fail. The two most common
+When you apply adjustments, some adjustments may fail. Some of the most common
errors you may encounter are:
- **#1406 Data Too Long**: Usually this is caused by a very long object name
(like a task title) which contains multibyte unicode characters. When the
column type is converted to `binary`, only the first part of the title still
fits in the column. Depending on what is failing, you may be able to find
the relevant object in the web UI and retitle it so the adjustment succeeds.
Alternatively, you can use `--unsafe` to force the adjustment to truncate
the title. This will destroy some data, but usually the data is not
important (just the end of very long titles).
- **#1366 Incorrect String Value**: This can occur when converting invalid
or truncated multibyte unicode characters to a unicode character set.
In both cases, the old value can not be represented under the new character
set. You may be able to identify the object and edit it to allow the
adjustment to proceed, or you can use the `--unsafe` flag to truncate the
data at the invalid character. Usually, the truncated data is not important.
As with most commands, you can add the `--trace` flag to get more details about
what `bin/storage adjust` is doing. This may help you diagnose or understand any
issues you encounter, and this data is useful if you file reports in the
upstream.
In general, adjustments are not critical. If you run into issues applying
adjustments, it is safe to file a task in the upstream describing the problem
you've encountered and continue using Phabricator normally until the issue can
be resolved.
+
+Surplus Schemata
+================
+
+After performing adjustment, you may receive an error that a table or column is
+"Surplus". The error looks something like this:
+
+| Target | Error |
+| --- | --- |
+| phabricator_example.example_table | Surplus |
+
+Generally, "Surplus" means that Phabricator does not expect the table or column
+to exist. These surpluses usually exist because you (or someone else
+with database access) added the table or column manually. Rarely, they can
+also exist for other reasons. They are usually safe to delete, but because
+deleting them destroys data and Phabricator can not be sure that the table or
+column doesn't have anything important in it, it does not delete them
+automatically.
+
+If you recognize the schema causing the issue as something you added and you
+don't need it anymore, you can safely delete it. If you aren't sure whether
+you added it or not, you can move the data somewhere else and delete it later.
+
+To move a table, first create a database for it like `my_backups`. Then, rename
+the table to move it into that database (use the table name given in the error
+message):
+
+```lang=sql
+CREATE DATABASE my_backups;
+RENAME TABLE phabricator_example.example_table
+ TO my_backups.example_table;
+```
+
+Phabricator will ignore tables that aren't in databases it owns, so you can
+safely move anything you aren't sure about outside of the Phabricator databases.
+
+If you're sure you don't need a table, use `DROP TABLE` to destroy it,
+specifying the correct table name (the one given in the error message):
+
+```lang=sql
+DROP TABLE phabricator_example.example_table;
+```
+
+This will destroy the table permanently.
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
index 87778dbe42..ba30d352fd 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
@@ -1,635 +1,674 @@
<?php
abstract class PhabricatorStorageManagementWorkflow
extends PhabricatorManagementWorkflow {
private $patches;
private $api;
public function setPatches(array $patches) {
assert_instances_of($patches, 'PhabricatorStoragePatch');
$this->patches = $patches;
return $this;
}
public function getPatches() {
return $this->patches;
}
final public function setAPI(PhabricatorStorageManagementAPI $api) {
$this->api = $api;
return $this;
}
final public function getAPI() {
return $this->api;
}
private function loadSchemata() {
$query = id(new PhabricatorConfigSchemaQuery())
->setAPI($this->getAPI());
$actual = $query->loadActualSchema();
$expect = $query->loadExpectedSchema();
$comp = $query->buildComparisonSchema($expect, $actual);
return array($comp, $expect, $actual);
}
protected function adjustSchemata($force, $unsafe, $dry_run) {
$console = PhutilConsole::getConsole();
$console->writeOut(
"%s\n",
pht('Verifying database schemata...'));
list($adjustments, $errors) = $this->findAdjustments();
$api = $this->getAPI();
if (!$adjustments) {
$console->writeOut(
"%s\n",
pht('Found no adjustments for schemata.'));
return $this->printErrors($errors, 0);
}
if (!$force && !$api->isCharacterSetAvailable('utf8mb4')) {
$message = pht(
"You have an old version of MySQL (older than 5.5) which does not ".
"support the utf8mb4 character set. If you apply adjustments now ".
"and later update MySQL to 5.5 or newer, you'll need to apply ".
"adjustments again (and they will take a long time).\n\n".
"You can exit this workflow, update MySQL now, and then run this ".
"workflow again. This is recommended, but may cause a lot of downtime ".
"right now.\n\n".
"You can exit this workflow, continue using Phabricator without ".
"applying adjustments, update MySQL at a later date, and then run ".
"this workflow again. This is also a good approach, and will let you ".
"delay downtime until later.\n\n".
"You can proceed with this workflow, and then optionally update ".
"MySQL at a later date. After you do, you'll need to apply ".
"adjustments again.\n\n".
"For more information, see \"Managing Storage Adjustments\" in ".
"the documentation.");
$console->writeOut(
"\n**<bg:yellow> %s </bg>**\n\n%s\n",
pht('OLD MySQL VERSION'),
phutil_console_wrap($message));
$prompt = pht('Continue with old MySQL version?');
if (!phutil_console_confirm($prompt, $default_no = true)) {
return;
}
}
$table = id(new PhutilConsoleTable())
->addColumn('database', array('title' => pht('Database')))
->addColumn('table', array('title' => pht('Table')))
->addColumn('name', array('title' => pht('Name')))
->addColumn('info', array('title' => pht('Issues')));
foreach ($adjustments as $adjust) {
$info = array();
foreach ($adjust['issues'] as $issue) {
$info[] = PhabricatorConfigStorageSchema::getIssueName($issue);
}
$table->addRow(array(
'database' => $adjust['database'],
'table' => idx($adjust, 'table'),
'name' => idx($adjust, 'name'),
'info' => implode(', ', $info),
));
}
$console->writeOut("\n\n");
$table->draw();
if ($dry_run) {
$console->writeOut(
"%s\n",
pht('DRYRUN: Would apply adjustments.'));
return 0;
} else if (!$force) {
$console->writeOut(
"\n%s\n",
pht(
"Found %s issues(s) with schemata, detailed above.\n\n".
"You can review issues in more detail from the web interface, ".
"in Config > Database Status. To better understand the adjustment ".
"workflow, see \"Managing Storage Adjustments\" in the ".
"documentation.\n\n".
"MySQL needs to copy table data to make some adjustments, so these ".
"migrations may take some time.",
new PhutilNumber(count($adjustments))));
$prompt = pht('Fix these schema issues?');
if (!phutil_console_confirm($prompt, $default_no = true)) {
return 1;
}
}
$console->writeOut(
"%s\n",
pht('Dropping caches, for faster migrations...'));
$root = dirname(phutil_get_library_root('phabricator'));
$bin = $root.'/bin/cache';
phutil_passthru('%s purge --purge-all', $bin);
$console->writeOut(
"%s\n",
pht('Fixing schema issues...'));
$conn = $api->getConn(null);
if ($unsafe) {
queryfx($conn, 'SET SESSION sql_mode = %s', '');
} else {
queryfx($conn, 'SET SESSION sql_mode = %s', 'STRICT_ALL_TABLES');
}
$failed = array();
// We make changes in several phases.
$phases = array(
// Drop surplus autoincrements. This allows us to drop primary keys on
// autoincrement columns.
'drop_auto',
// Drop all keys we're going to adjust. This prevents them from
// interfering with column changes.
'drop_keys',
// Apply all database, table, and column changes.
'main',
// Restore adjusted keys.
'add_keys',
// Add missing autoincrements.
'add_auto',
);
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($adjustments) * count($phases));
foreach ($phases as $phase) {
foreach ($adjustments as $adjust) {
try {
switch ($adjust['kind']) {
case 'database':
if ($phase == 'main') {
queryfx(
$conn,
'ALTER DATABASE %T CHARACTER SET = %s COLLATE = %s',
$adjust['database'],
$adjust['charset'],
$adjust['collation']);
}
break;
case 'table':
if ($phase == 'main') {
queryfx(
$conn,
'ALTER TABLE %T.%T COLLATE = %s',
$adjust['database'],
$adjust['table'],
$adjust['collation']);
}
break;
case 'column':
$apply = false;
$auto = false;
$new_auto = idx($adjust, 'auto');
if ($phase == 'drop_auto') {
if ($new_auto === false) {
$apply = true;
$auto = false;
}
} else if ($phase == 'main') {
$apply = true;
if ($new_auto === false) {
$auto = false;
} else {
$auto = $adjust['is_auto'];
}
} else if ($phase == 'add_auto') {
if ($new_auto === true) {
$apply = true;
$auto = true;
}
}
if ($apply) {
$parts = array();
if ($auto) {
$parts[] = qsprintf(
$conn,
'AUTO_INCREMENT');
}
if ($adjust['charset']) {
$parts[] = qsprintf(
$conn,
'CHARACTER SET %Q COLLATE %Q',
$adjust['charset'],
$adjust['collation']);
}
queryfx(
$conn,
'ALTER TABLE %T.%T MODIFY %T %Q %Q %Q',
$adjust['database'],
$adjust['table'],
$adjust['name'],
$adjust['type'],
implode(' ', $parts),
$adjust['nullable'] ? 'NULL' : 'NOT NULL');
}
break;
case 'key':
if (($phase == 'drop_keys') && $adjust['exists']) {
if ($adjust['name'] == 'PRIMARY') {
$key_name = 'PRIMARY KEY';
} else {
$key_name = qsprintf($conn, 'KEY %T', $adjust['name']);
}
queryfx(
$conn,
'ALTER TABLE %T.%T DROP %Q',
$adjust['database'],
$adjust['table'],
$key_name);
}
if (($phase == 'add_keys') && $adjust['keep']) {
// Different keys need different creation syntax. Notable
// special cases are primary keys and fulltext keys.
if ($adjust['name'] == 'PRIMARY') {
$key_name = 'PRIMARY KEY';
} else if ($adjust['indexType'] == 'FULLTEXT') {
$key_name = qsprintf($conn, 'FULLTEXT %T', $adjust['name']);
} else {
if ($adjust['unique']) {
$key_name = qsprintf(
$conn,
'UNIQUE KEY %T',
$adjust['name']);
} else {
$key_name = qsprintf(
$conn,
'/* NONUNIQUE */ KEY %T',
$adjust['name']);
}
}
queryfx(
$conn,
'ALTER TABLE %T.%T ADD %Q (%Q)',
$adjust['database'],
$adjust['table'],
$key_name,
implode(', ', $adjust['columns']));
}
break;
default:
throw new Exception(
pht('Unknown schema adjustment kind "%s"!', $adjust['kind']));
}
} catch (AphrontQueryException $ex) {
$failed[] = array($adjust, $ex);
}
$bar->update(1);
}
}
$bar->done();
if (!$failed) {
$console->writeOut(
"%s\n",
pht('Completed fixing all schema issues.'));
$err = 0;
} else {
$table = id(new PhutilConsoleTable())
->addColumn('target', array('title' => pht('Target')))
->addColumn('error', array('title' => pht('Error')));
foreach ($failed as $failure) {
list($adjust, $ex) = $failure;
$pieces = array_select_keys(
$adjust,
array('database', 'table', 'name'));
$pieces = array_filter($pieces);
$target = implode('.', $pieces);
$table->addRow(
array(
'target' => $target,
'error' => $ex->getMessage(),
));
}
$console->writeOut("\n");
$table->draw();
$console->writeOut(
"\n%s\n",
pht('Failed to make some schema adjustments, detailed above.'));
$console->writeOut(
"%s\n",
pht(
'For help troubleshooting adjustments, see "Managing Storage '.
'Adjustments" in the documentation.'));
$err = 1;
}
return $this->printErrors($errors, $err);
}
private function findAdjustments() {
list($comp, $expect, $actual) = $this->loadSchemata();
$issue_charset = PhabricatorConfigStorageSchema::ISSUE_CHARSET;
$issue_collation = PhabricatorConfigStorageSchema::ISSUE_COLLATION;
$issue_columntype = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE;
$issue_surpluskey = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY;
$issue_missingkey = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY;
$issue_columns = PhabricatorConfigStorageSchema::ISSUE_KEYCOLUMNS;
$issue_unique = PhabricatorConfigStorageSchema::ISSUE_UNIQUE;
$issue_longkey = PhabricatorConfigStorageSchema::ISSUE_LONGKEY;
$issue_auto = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT;
$adjustments = array();
$errors = array();
foreach ($comp->getDatabases() as $database_name => $database) {
foreach ($this->findErrors($database) as $issue) {
$errors[] = array(
'database' => $database_name,
'issue' => $issue,
);
}
$expect_database = $expect->getDatabase($database_name);
$actual_database = $actual->getDatabase($database_name);
if (!$expect_database || !$actual_database) {
// If there's a real issue here, skip this stuff.
continue;
}
$issues = array();
if ($database->hasIssue($issue_charset)) {
$issues[] = $issue_charset;
}
if ($database->hasIssue($issue_collation)) {
$issues[] = $issue_collation;
}
if ($issues) {
$adjustments[] = array(
'kind' => 'database',
'database' => $database_name,
'issues' => $issues,
'charset' => $expect_database->getCharacterSet(),
'collation' => $expect_database->getCollation(),
);
}
foreach ($database->getTables() as $table_name => $table) {
foreach ($this->findErrors($table) as $issue) {
$errors[] = array(
'database' => $database_name,
'table' => $table_name,
'issue' => $issue,
);
}
$expect_table = $expect_database->getTable($table_name);
$actual_table = $actual_database->getTable($table_name);
if (!$expect_table || !$actual_table) {
continue;
}
$issues = array();
if ($table->hasIssue($issue_collation)) {
$issues[] = $issue_collation;
}
if ($issues) {
$adjustments[] = array(
'kind' => 'table',
'database' => $database_name,
'table' => $table_name,
'issues' => $issues,
'collation' => $expect_table->getCollation(),
);
}
foreach ($table->getColumns() as $column_name => $column) {
foreach ($this->findErrors($column) as $issue) {
$errors[] = array(
'database' => $database_name,
'table' => $table_name,
'name' => $column_name,
'issue' => $issue,
);
}
$expect_column = $expect_table->getColumn($column_name);
$actual_column = $actual_table->getColumn($column_name);
if (!$expect_column || !$actual_column) {
continue;
}
$issues = array();
if ($column->hasIssue($issue_collation)) {
$issues[] = $issue_collation;
}
if ($column->hasIssue($issue_charset)) {
$issues[] = $issue_charset;
}
if ($column->hasIssue($issue_columntype)) {
$issues[] = $issue_columntype;
}
if ($column->hasIssue($issue_auto)) {
$issues[] = $issue_auto;
}
if ($issues) {
if ($expect_column->getCharacterSet() === null) {
// For non-text columns, we won't be specifying a collation or
// character set.
$charset = null;
$collation = null;
} else {
$charset = $expect_column->getCharacterSet();
$collation = $expect_column->getCollation();
}
$adjustment = array(
'kind' => 'column',
'database' => $database_name,
'table' => $table_name,
'name' => $column_name,
'issues' => $issues,
'collation' => $collation,
'charset' => $charset,
'type' => $expect_column->getColumnType(),
// NOTE: We don't adjust column nullability because it is
// dangerous, so always use the current nullability.
'nullable' => $actual_column->getNullable(),
// NOTE: This always stores the current value, because we have
// to make these updates separately.
'is_auto' => $actual_column->getAutoIncrement(),
);
if ($column->hasIssue($issue_auto)) {
$adjustment['auto'] = $expect_column->getAutoIncrement();
}
$adjustments[] = $adjustment;
}
}
foreach ($table->getKeys() as $key_name => $key) {
foreach ($this->findErrors($key) as $issue) {
$errors[] = array(
'database' => $database_name,
'table' => $table_name,
'name' => $key_name,
'issue' => $issue,
);
}
$expect_key = $expect_table->getKey($key_name);
$actual_key = $actual_table->getKey($key_name);
$issues = array();
$keep_key = true;
if ($key->hasIssue($issue_surpluskey)) {
$issues[] = $issue_surpluskey;
$keep_key = false;
}
if ($key->hasIssue($issue_missingkey)) {
$issues[] = $issue_missingkey;
}
if ($key->hasIssue($issue_columns)) {
$issues[] = $issue_columns;
}
if ($key->hasIssue($issue_unique)) {
$issues[] = $issue_unique;
}
// NOTE: We can't really fix this, per se, but we may need to remove
// the key to change the column type. In the best case, the new
// column type won't be overlong and recreating the key really will
// fix the issue. In the worst case, we get the right column type and
// lose the key, which is still better than retaining the key having
// the wrong column type.
if ($key->hasIssue($issue_longkey)) {
$issues[] = $issue_longkey;
}
if ($issues) {
$adjustment = array(
'kind' => 'key',
'database' => $database_name,
'table' => $table_name,
'name' => $key_name,
'issues' => $issues,
'exists' => (bool)$actual_key,
'keep' => $keep_key,
);
if ($keep_key) {
$adjustment += array(
'columns' => $expect_key->getColumnNames(),
'unique' => $expect_key->getUnique(),
'indexType' => $expect_key->getIndexType(),
);
}
$adjustments[] = $adjustment;
}
}
}
}
return array($adjustments, $errors);
}
private function findErrors(PhabricatorConfigStorageSchema $schema) {
$result = array();
foreach ($schema->getLocalIssues() as $issue) {
$status = PhabricatorConfigStorageSchema::getIssueStatus($issue);
if ($status == PhabricatorConfigStorageSchema::STATUS_FAIL) {
$result[] = $issue;
}
}
return $result;
}
private function printErrors(array $errors, $default_return) {
if (!$errors) {
return $default_return;
}
$console = PhutilConsole::getConsole();
$table = id(new PhutilConsoleTable())
->addColumn('target', array('title' => pht('Target')))
->addColumn('error', array('title' => pht('Error')));
+ $any_surplus = false;
+ $all_surplus = true;
foreach ($errors as $error) {
$pieces = array_select_keys(
$error,
array('database', 'table', 'name'));
$pieces = array_filter($pieces);
$target = implode('.', $pieces);
$name = PhabricatorConfigStorageSchema::getIssueName($error['issue']);
+ if ($error['issue'] === PhabricatorConfigStorageSchema::ISSUE_SURPLUS) {
+ $any_surplus = true;
+ } else {
+ $all_surplus = false;
+ }
+
$table->addRow(
array(
'target' => $target,
'error' => $name,
));
}
$console->writeOut("\n");
$table->draw();
$console->writeOut("\n");
- $message = pht(
- "The schemata have serious errors (detailed above) which the adjustment ".
- "workflow can not fix.\n\n".
- "If you are not developing Phabricator itself, report this issue to ".
- "the upstream.\n\n".
- "If you are developing Phabricator, these errors usually indicate that ".
- "your schema specifications do not agree with the schemata your code ".
- "actually builds.");
- $console->writeOut(
- "**<bg:red> %s </bg>**\n\n%s\n",
- pht('SCHEMATA ERRORS'),
- phutil_console_wrap($message));
+ $message = array();
+ if ($all_surplus) {
+ $message[] = pht(
+ 'You have surplus schemata (extra tables or columns which Phabricator '.
+ 'does not expect). For information on resolving these '.
+ 'issues, see the "Surplus Schemata" section in the "Managing Storage '.
+ 'Adjustments" article in the documentation.');
+ } else {
+ $message[] = pht(
+ 'The schemata have errors (detailed above) which the adjustment '.
+ 'workflow can not fix.');
+
+ if ($any_surplus) {
+ $message[] = pht(
+ 'Some of these errors are caused by surplus schemata (extra '.
+ 'tables or columsn which Phabricator does not expect). These are '.
+ 'not serious. For information on resolving these issues, see the '.
+ '"Surplus Schemata" section in the "Managing Storage Adjustments" '.
+ 'article in the documentation.');
+ }
+
+ $message[] = pht(
+ 'If you are not developing Phabricator itself, report this issue to '.
+ 'the upstream.');
+
+ $message[] = pht(
+ 'If you are developing Phabricator, these errors usually indicate '.
+ 'that your schema specifications do not agree with the schemata your '.
+ 'code actually builds.');
+ }
+ $message = implode("\n\n", $message);
+
+ if ($all_surplus) {
+ $console->writeOut(
+ "**<bg:yellow> %s </bg>**\n\n%s\n",
+ pht('SURPLUS SCHEMATA'),
+ phutil_console_wrap($message));
+ } else {
+ $console->writeOut(
+ "**<bg:red> %s </bg>**\n\n%s\n",
+ pht('SCHEMATA ERRORS'),
+ phutil_console_wrap($message));
+ }
return 2;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Jul 3, 3:55 PM (5 h, 59 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
165992
Default Alt Text
(30 KB)

Event Timeline