Page MenuHomePhabricator

undo_transactions.php

Authored By
epriestley
Jul 1 2016, 12:47 PM
Size
10 KB
Referenced Files
None
Subscribers
None

undo_transactions.php

#!/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.'));
}

File Metadata

Mime Type
text/plain; charset=utf-8
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
193062
Default Alt Text
undo_transactions.php (10 KB)

Event Timeline