#!/usr/bin/env php setTagline(pht('undo transactions created by a repository import')); $args->setSynopsis(<<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( "** %s ** %s\n", pht('TARGETS'), pht( 'Transactions originating from these repositories will be undone: %s.', implode(', ', mpull($repositories, 'getDisplayName')))); echo tsprintf( "** %s ** %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( "** %s ** %s\n", pht('TASK'), pht( 'Examining task: %s %s.', $task->getMonogram(), $task->getTitle())); $status = $task->getStatus(); if (ManiphestTaskStatus::isOpenStatus($status)) { echo tsprintf( "** %s ** %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( "** %s ** %s\n", pht('FIXED'), pht( 'Task had its status changed after the incident, and will not be '. 'mutated.')); continue; } if (!$status_xaction) { echo tsprintf( "** %s ** %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( "** %s ** %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( "** %s ** %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( "** %s ** %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( "** %s ** %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( "** %s ** %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( "** %s ** %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( "** %s ** %s\n", pht('UNDO TRANSCATIONS'), pht( 'Transactions will be deleted: %s.', implode(', ', $will_delete))); if (!$is_write) { echo tsprintf( "** %s ** %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( "** %s ** %s\n", pht('UNDONE'), pht('Mutated task to pre-incident state.')); }