diff --git a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php index cbb7d657af..62f2731064 100644 --- a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php @@ -1,257 +1,269 @@ ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'resolves' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'fix' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'fixes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'wontfixes' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'wontfixed' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'spites' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'spited' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'invalidate' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, 'invaldiates' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, 'invalidated' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, 'close' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'closes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'closed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'ref' => null, 'refs' => null, 'references' => null, 'cf.' => null, ); $suffixes = array( 'as resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'as fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'as wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'as spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'out of spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'as invalid' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, '' => null, ); $prefix_regex = array(); foreach ($prefixes as $prefix => $resolution) { $prefix_regex[] = preg_quote($prefix, '/'); } $prefix_regex = implode('|', $prefix_regex); $suffix_regex = array(); foreach ($suffixes as $suffix => $resolution) { $suffix_regex[] = preg_quote($suffix, '/'); } $suffix_regex = implode('|', $suffix_regex); $matches = null; preg_match_all( "/({$prefix_regex})\s+T(\d+)\s*({$suffix_regex})/i", $message, $matches, PREG_SET_ORDER); $tasks_statuses = array(); foreach ($matches as $set) { $prefix = strtolower($set[1]); $task_id = (int)$set[2]; $suffix = strtolower($set[3]); $status = idx($suffixes, $suffix); if (!$status) { $status = idx($prefixes, $prefix); } $tasks_statuses[$task_id] = $status; } return $tasks_statuses; } private function findDependentRevisions($message) { $dependents = array(); $matches = null; preg_match_all( '/\b(?i:depends\s+on)\s+D(\d+(,\s+D\d++)*)\b/', $message, $matches); foreach ($matches[1] as $revisions) { foreach (preg_split('/,\s+D/', $revisions) as $id) { $dependents[$id] = $id; } } return $dependents; } public static function findRevertedCommits($message) { $reverts = array(); $matches = null; // NOTE: Git language is "This reverts commit X." // NOTE: Mercurial language is "Backed out changeset Y". $prefixes = array( 'revert' => true, 'reverts' => true, 'back\s*out' => true, 'backs\s*out' => true, 'backed\s*out' => true, 'undo' => true, 'undoes' => true, ); $optional = array( 'commit' => true, 'changeset' => true, 'rev' => true, 'revision' => true, 'change' => true, 'diff' => true, ); $pre_re = implode('|', array_keys($prefixes)); $opt_re = implode('|', array_keys($optional)); $matches = null; preg_match_all( '/\b(?i:'.$pre_re.')(?:\s+(?i:'.$opt_re.'))?([rA-Z0-9a-f,\s]+)\b/', $message, $matches); $result = array(); foreach ($matches[1] as $commits) { $commits = preg_split('/[,\s]+/', $commits); $commits = array_filter($commits); foreach ($commits as $commit) { $result[$commit] = $commit; } } return $result; } public function didWriteRevision(DifferentialRevisionEditor $editor) { $message = $this->renderValueForCommitMessage(false); $tasks = $this->findMentionedTasks($message); if ($tasks) { $tasks = id(new ManiphestTask()) ->loadAllWhere('id IN (%Ld)', array_keys($tasks)); $this->saveFieldEdges( $editor->getRevision(), PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK, mpull($tasks, 'getPHID')); } $dependents = $this->findDependentRevisions($message); if ($dependents) { $dependents = id(new DifferentialRevision()) ->loadAllWhere('id IN (%Ld)', $dependents); $this->saveFieldEdges( $editor->getRevision(), PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV, mpull($dependents, 'getPHID')); } } private function saveFieldEdges( DifferentialRevision $revision, $edge_type, array $add_phids) { $revision_phid = $revision->getPHID(); $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $revision_phid, $edge_type); $add_phids = array_diff($add_phids, $old_phids); if (!$add_phids) { return; } $edge_editor = id(new PhabricatorEdgeEditor())->setActor($this->getUser()); foreach ($add_phids as $phid) { $edge_editor->addEdge($revision_phid, $edge_type, $phid); } // NOTE: Deletes only through the fields. $edge_editor->save(); } public function didParseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { $message = $this->renderValueForCommitMessage($is_edit = false); - $commits = self::findRevertedCommits($message); - $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $data->getCommitDetail('authorPHID')); if (!$user) { + // TODO: Maybe after grey users, we should find a way to proceed even + // if we don't know who the author is. return; } + $commit_names = self::findRevertedCommits($message); + if ($commit_names) { + $reverts = id(new DiffusionCommitQuery()) + ->setViewer($user) + ->withIdentifiers($commit_names) + ->withDefaultRepository($repository) + ->execute(); + foreach ($reverts as $revert) { + // TODO: Do interesting things here. + } + } + $tasks_statuses = $this->findMentionedTasks($message); if (!$tasks_statuses) { return; } $tasks = id(new ManiphestTaskQuery()) ->withTaskIDs(array_keys($tasks_statuses)) ->execute(); foreach ($tasks as $task_id => $task) { id(new PhabricatorEdgeEditor()) ->setActor($user) ->addEdge( $task->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT, $commit->getPHID()) ->save(); $status = $tasks_statuses[$task_id]; if (!$status) { // Text like "Ref T123", don't change the task status. continue; } if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { // Task is already closed. continue; } $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $call = new ConduitCall( 'maniphest.update', array( 'id' => $task->getID(), 'status' => $status, 'comments' => "Closed by commit {$commit_name}.", )); $call->setUser($user); $call->execute(); } } } diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index 789aeb1f59..c9f757aee8 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -1,156 +1,177 @@ identifiers = $identifiers; return $this; } + /** + * If a default repository is provided, ambiguous commit identifiers will + * be assumed to belong to the default repository. + * + * For example, "r123" appearing in a commit message in repository X is + * likely to be unambiguously "rX123". Normally the reference would be + * considered ambiguous, but if you provide a default repository it will + * be correctly resolved. + */ + public function withDefaultRepository(PhabricatorRepository $repository) { + $this->defaultRepository = $repository; + return $this; + } + public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } protected function loadPage() { $table = new PhabricatorRepositoryCommit(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } public function willFilterPage(array $commits) { if (!$commits) { return array(); } $repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIDs($repository_ids) ->execute(); foreach ($commits as $key => $commit) { $repo = idx($repos, $commit->getRepositoryID()); if ($repo) { $commit->attachRepository($repo); } else { unset($commits[$key]); } } return $commits; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->identifiers) { $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH; $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; $refs = array(); $bare = array(); foreach ($this->identifiers as $identifier) { $matches = null; preg_match('/^(?:r([A-Z]+))?(.*)$/', $identifier, $matches); $repo = nonempty($matches[1], null); $identifier = nonempty($matches[2], null); + if ($repo === null) { + if ($this->defaultRepository) { + $repo = $this->defaultRepository->getCallsign(); + } + } + if ($repo === null) { if (strlen($identifier) < $min_unqualified) { continue; } $bare[] = $identifier; } else { $refs[] = array( 'callsign' => $repo, 'identifier' => $identifier, ); } } $sql = array(); foreach ($bare as $identifier) { $sql[] = qsprintf( $conn_r, '(commitIdentifier LIKE %> AND LENGTH(commitIdentifier) = 40)', $identifier); } if ($refs) { $callsigns = ipull($refs, 'callsign'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withCallsigns($callsigns) ->execute(); $repos = mpull($repos, null, 'getCallsign'); foreach ($refs as $key => $ref) { $repo = idx($repos, $ref['callsign']); if (!$repo) { continue; } if ($repo->isSVN()) { if (!ctype_digit($ref['identifier'])) { continue; } $sql[] = qsprintf( $conn_r, '(repositoryID = %d AND commitIdentifier = %d)', $repo->getID(), $ref['identifier']); } else { if (strlen($ref['identifier']) < $min_qualified) { continue; } $sql[] = qsprintf( $conn_r, '(repositoryID = %d AND commitIdentifier LIKE %>)', $repo->getID(), $ref['identifier']); } } } if (!$sql) { // If we discarded all possible identifiers (e.g., they all referenced // bogus repositories or were all too short), make sure the query finds // nothing. throw new PhabricatorEmptyQueryException('No commit identifiers.'); } $where[] = '('.implode(' OR ', $sql).')'; } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } return $this->formatWhereClause($where); } }