Page MenuHomePhabricator
Paste P1997

undo_transactions.php
ActivePublic

Authored by epriestley on Jul 1 2016, 12:47 PM.
Tags
None
Referenced Files
F1707826: Screen Shot 2016-07-01 at 5.55.59 AM.png
Jul 1 2016, 12:56 PM
F1707818: undo_transactions.php
Jul 1 2016, 12:47 PM
Subscribers
None
Tokens
"Baby Tequila" token, awarded by chad.
#!/usr/bin/env php
<?php
require_once 'scripts/__init_script__.php';
// Configure a list of problem repository IDs. "Fixes Txxx" will only be undone
// if they came from these repositories.
$repository_ids = array(1, 2, 3);
// Configure the start and end times for the incident. Transactions which
// applied between these times will be undone, but older or newer transactions
// will not.
$epoch_start = PhabricatorTime::getNow() - phutil_units('1 day in seconds');
$epoch_end = PhabricatorTime::getNow();
// Probably no need to change this; this is a heuristic for identifying that
// transactions happened in the same group by observing that they happend very
// close together (transaction groups aren't stored formally).
$a_few_seconds = 15;
$args = new PhutilArgumentParser($argv);
$args->setTagline(pht('undo transactions created by a repository import'));
$args->setSynopsis(<<<EOHELP
**undo_transactions.php** T123
**undo_transactions.php** --all
Undo "Fixes" transactions from configured repositories.
By default, shows you what would be done. With --write, actually does it.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'write',
'help' => pht('Actually repair damage instead of just showing it.'),
),
array(
'name' => 'all',
'help' => pht('Process ALL tasks.'),
),
array(
'name' => 'tasks',
'wildcard' => true,
),
));
$viewer = PhabricatorUser::getOmnipotentUser();
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIDs($repository_ids)
->execute();
$repositories = mpull($repositories, null, 'getID');
foreach ($repository_ids as $repository_id) {
if (empty($repositories[$repository_id])) {
throw new PhutilArgumentUsageException(
pht(
'Configured repository ID "%s" does not correspond to a loadable '.
'repository.',
$repository_id));
}
}
$all = $args->getArg('all');
$monograms = $args->getArg('tasks');
if ($all && $monograms) {
throw new PhutilArgumentUsageException(
pht(
'Specify either "--all" or a list of tasks, but not both.'));
} else if (!$all && !$monograms) {
throw new PhutilArgumentUsageException(
pht(
'Specify "--all" or a list of tasks to act on.'));
} else if ($all) {
$tasks = new LiskMigrationIterator(new ManiphestTask());
} else {
$ids = array();
foreach ($monograms as $monogram) {
if (!preg_match('/^T\d+\z/', $monogram)) {
throw new PhutilArgumentUsageException(
pht(
'When providing a list of tasks, they should be in the form '.
'"T123". Provided task "%s" is not.',
$monogram));
}
$ids[$monogram] = trim($monogram, 'T');
}
$tasks = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs($ids)
->execute();
$tasks = mpull($tasks, null, 'getID');
foreach ($ids as $monogram => $id) {
if (empty($tasks[$id])) {
throw new PhutilArgumentUsageException(
pht(
'Task "%s" is not a valid, loadable task.',
$monogram));
}
}
}
$is_write = $args->getArg('write');
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('TARGETS'),
pht(
'Transactions originating from these repositories will be undone: %s.',
implode(', ', mpull($repositories, 'getDisplayName'))));
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('RANGE'),
pht(
'Transactions between %s and %s will be undone.',
phabricator_datetime($epoch_start, $viewer),
phabricator_datetime($epoch_end, $viewer)));
foreach ($tasks as $task) {
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('TASK'),
pht(
'Examining task: %s %s.',
$task->getMonogram(),
$task->getTitle()));
$status = $task->getStatus();
if (ManiphestTaskStatus::isOpenStatus($status)) {
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('STATUS'),
pht(
'Task has an open status ("%s") and will not be mutated.',
$status));
continue;
}
$xactions = id(new ManiphestTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(array($task->getPHID()))
->execute();
$status_xaction = null;
$owner_xactions = array();
$edge_xactions = array();
$already_fixed = null;
foreach ($xactions as $xaction) {
$created = $xaction->getDateCreated();
$type = $xaction->getTransactionType();
// If this transaction happened after the incident, we aren't going to
// undo it.
if ($created > $epoch_end) {
// If it's a status transaction, the task status has been updated
// after the incident, so we don't want to undo that. Stop looking for
// stuff to fix.
if ($type == ManiphestTransaction::TYPE_STATUS) {
$already_fixed = $xaction;
break;
}
continue;
}
// If this happened before the incident then we're done picking through
// the rubble.
if ($created < $epoch_start) {
break;
}
switch ($type) {
case ManiphestTransaction::TYPE_STATUS:
if (!$status_xaction) {
if ($xaction->getMetadataValue('commitPHID')) {
$status_xaction = $xaction;
}
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $xaction->getMetadataValue('edge:type');
if ($edge_type == ManiphestTaskHasCommitEdgeType::EDGECONST) {
if ($xaction->getMetadataValue('commitPHID')) {
$edge_xactions[] = $xaction;
}
}
break;
case ManiphestTransaction::TYPE_OWNER:
$owner_xactions[] = $xaction;
break;
}
}
if ($already_fixed) {
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('FIXED'),
pht(
'Task had its status changed after the incident, and will not be '.
'mutated.'));
continue;
}
if (!$status_xaction) {
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('UNAFFECTED'),
pht(
'Task was not closed during the incident, and will not be mutated.'));
continue;
}
// Find an edge transaction which added commits within a few seconds of the
// status transaction, if one exists.
$edge_xaction = null;
$status_date = $status_xaction->getDateCreated();
foreach ($edge_xactions as $xaction) {
$edge_date = $xaction->getDateCreated();
$delta = abs($edge_date - $status_date);
if ($delta < $a_few_seconds) {
$edge_xaction = $xaction;
break;
}
}
if (!$edge_xaction) {
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('NO COMMITS'),
pht(
'Task was closed during the incident, but no commits were attached '.
'at similar times. This looks like an unrelated status change, so it '.
'will not be mutated.'));
continue;
}
$old_commits = $edge_xaction->getOldValue();
if (!is_array($old_commits)) {
$old_commits = array();
}
$old_commits = array_keys($old_commits);
$new_commits = $edge_xaction->getNewValue();
if (!is_array($new_commits)) {
$new_commits = array();
}
$new_commits = array_keys($new_commits);
$add_commits = array_diff($new_commits, $old_commits);
$commits = array();
if ($add_commits) {
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withPHIDs($add_commits)
->execute();
}
if (!$commits) {
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('NO ADDED COMMITS'),
pht(
'Task was closed during the incident and commits were changed, but '.
'none were added. This looks like a coincidence, so the task will '.
'not be mutated.'));
continue;
}
$in_repository = array();
foreach ($commits as $commit) {
$repository_id = $commit->getRepository()->getID();
if (isset($repositories[$repository_id])) {
$in_repository[] = $commit;
}
}
if (!$in_repository) {
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('NO REPOSITORY COMMITS'),
pht(
'Task was closed during the incident and commits were added, but '.
'not from the specified repositories. This looks unrelated, so the '.
'task will not be mutated.'));
continue;
}
$owner_xaction = null;
foreach ($owner_xactions as $xaction) {
$owner_date = $xaction->getDateCreated();
$delta = abs($edge_date - $owner_date);
if ($delta < $a_few_seconds) {
$owner_xaction = $xaction;
break;
}
}
// We're ready to undo damage: we have a closing transaction in the incident
// window that applied adjacent to commits from the repository being
// attached. We're going to undo the status change, remove the commits, and
// destroy the transaction.
echo tsprintf(
"**<bg:yellow> %s </bg>** %s\n",
pht('UNDO STATUS'),
pht(
'Task was affected, status will be reverted from "%s" to "%s".',
$status_xaction->getNewValue(),
$status_xaction->getOldValue()));
if ($owner_xaction) {
echo tsprintf(
"**<bg:yellow> %s </bg>** %s\n",
pht('UNDO OWNER'),
pht(
'Owner will be reverted from "%s" to "%s".',
nonempty($owner_xaction->getNewValue(), pht('None')),
nonempty($owner_xaction->getOldValue(), pht('None'))));
}
echo tsprintf(
"**<bg:yellow> %s </bg>** %s\n",
pht('UNDO COMMITS'),
pht(
'Commits will be unlinked: %s.',
implode(', ', mpull($in_repository, 'getDisplayName'))));
$will_delete = array(
$status_xaction->getID(),
$edge_xaction->getID(),
);
if ($owner_xaction) {
$will_delete[] = $owner_xaction->getID();
}
echo tsprintf(
"**<bg:yellow> %s </bg>** %s\n",
pht('UNDO TRANSCATIONS'),
pht(
'Transactions will be deleted: %s.',
implode(', ', $will_delete)));
if (!$is_write) {
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('NO ACTION'),
pht(
'This command was not run with --write, so no actual action will '.
'be taken.'));
continue;
}
// Revert the status.
$task->setStatus($status_xaction->getOldValue());
// Revert the owner.
if ($owner_xaction) {
$task->setOwnerPHID($owner_xaction->getOldValue());
}
$task->save();
// Detach the commits.
$edge_editor = new PhabricatorEdgeEditor();
foreach ($in_repository as $commit) {
$edge_editor->removeEdge(
$task->getPHID(),
ManiphestTaskHasCommitEdgeType::EDGECONST,
$commit->getPHID());
}
$edge_editor->save();
// Destroy the transactions.
$status_xaction->delete();
$edge_xaction->delete();
if ($owner_xaction) {
$owner_xaction->delete();
}
echo tsprintf(
"**<bg:red> %s </bg>** %s\n",
pht('UNDONE'),
pht('Mutated task to pre-incident state.'));
}

Event Timeline

epriestley changed the visibility from "All Users" to "Public (No Login Required)".
epriestley changed the edit policy from "All Users" to "Community (Project)".
IMPORTANT: There is no guarantee this works properly, especially if any amount of time has passed since the script was written. This scripts is NOT maintained by the upstream. Run at your own risk!

Configuration

This script undoes side effects of "Fixes Txxx" (and similar) if a lot of commits from some other source are pushed to a tracked + autoclose repository in a way that doesn't trigger import mode.

  • Put this script in phabricator/.
  • Give it chmod +x.
  • At the top of the script:
    • Configure $repository_ids to be a list of repository IDs where problem commits were pushed.
    • Set $epoch_start to the beginning of the incident (default: 24 hours ago).
    • Set $epoch_end to the end of the incident (default: now).

Execution

The script is safe (performs no writes) as long as you do not pass --write. The --write flag is dangerous and permanently destroys data.

To begin with, identify one affected task. Run this to see how it would be repaired:

phabricator/ $ ./undo_transactions.php T123

If that looks good, repair the task:

phabricator/ $ ./undo_transactions.php T123 --write

If that looks good too, do it a few more times on other tasks, then preview the effects of repairing everything like this:

phabricator/ $ ./undo_transactions.php --all

Finally, repair everything:

phabricator/ $ ./undo_transactions.php --all --write

Example

Here's an example of the output:

Screen Shot 2016-07-01 at 5.55.59 AM.png (300×1 px, 103 KB)