1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-15 10:00:55 +01:00

(stable) Promote 2017 Week 38

This commit is contained in:
epriestley 2017-09-22 11:48:44 -07:00
commit 7ae4d93043
40 changed files with 816 additions and 222 deletions

View file

@ -0,0 +1,6 @@
CREATE TABLE {$NAMESPACE}_repository.repository_refposition (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
cursorID INT UNSIGNED NOT NULL,
commitIdentifier VARCHAR(40) NOT NULL COLLATE {$COLLATE_TEXT},
isClosed BOOL NOT NULL
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,71 @@
<?php
$table = new PhabricatorRepositoryRefCursor();
$conn = $table->establishConnection('w');
$map = array();
foreach (new LiskMigrationIterator($table) as $ref) {
$repository_phid = $ref->getRepositoryPHID();
$ref_type = $ref->getRefType();
$ref_hash = $ref->getRefNameHash();
$ref_key = "{$repository_phid}/{$ref_type}/{$ref_hash}";
if (!isset($map[$ref_key])) {
$map[$ref_key] = array(
'id' => $ref->getID(),
'type' => $ref_type,
'hash' => $ref_hash,
'repositoryPHID' => $repository_phid,
'positions' => array(),
);
}
// NOTE: When this migration runs, the table will have "commitIdentifier" and
// "isClosed" fields. Later, it won't. Since they'll be removed, we can't
// rely on being able to access them via the object. Instead, run a separate
// raw query to read them.
$row = queryfx_one(
$conn,
'SELECT commitIdentifier, isClosed FROM %T WHERE id = %d',
$ref->getTableName(),
$ref->getID());
$map[$ref_key]['positions'][] = array(
'identifier' => $row['commitIdentifier'],
'isClosed' => (int)$row['isClosed'],
);
}
// Now, write all the position rows.
$position_table = new PhabricatorRepositoryRefPosition();
foreach ($map as $ref_key => $spec) {
$id = $spec['id'];
foreach ($spec['positions'] as $position) {
queryfx(
$conn,
'INSERT IGNORE INTO %T (cursorID, commitIdentifier, isClosed)
VALUES (%d, %s, %d)',
$position_table->getTableName(),
$id,
$position['identifier'],
$position['isClosed']);
}
}
// Finally, delete all the redundant RefCursor rows (rows with the same name)
// so we can add proper unique keys in the next migration.
foreach ($map as $ref_key => $spec) {
queryfx(
$conn,
'DELETE FROM %T WHERE refType = %s
AND refNameHash = %s
AND repositoryPHID = %s
AND id != %d',
$table->getTableName(),
$spec['type'],
$spec['hash'],
$spec['repositoryPHID'],
$spec['id']);
}

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_repository.repository_refcursor
DROP COLUMN commitIdentifier;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_repository.repository_refcursor
DROP COLUMN isClosed;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_repository.repository_refcursor
ADD UNIQUE KEY `key_ref` (repositoryPHID, refType, refNameHash);

View file

@ -0,0 +1,52 @@
<?php
$table = new PhabricatorRepositoryRefPosition();
$conn = $table->establishConnection('w');
$key_name = 'key_position';
try {
queryfx(
$conn,
'ALTER TABLE %T DROP KEY %T',
$table->getTableName(),
$key_name);
} catch (AphrontQueryException $ex) {
// This key may or may not exist, depending on exactly when the install
// ran previous migrations and adjustments. We're just dropping it if it
// does exist.
// We're doing this first (outside of the lock) because the MySQL
// documentation says "if you ALTER TABLE a locked table, it may become
// unlocked".
}
queryfx(
$conn,
'LOCK TABLES %T WRITE',
$table->getTableName());
$seen = array();
foreach (new LiskMigrationIterator($table) as $position) {
$cursor_id = $position->getCursorID();
$hash = $position->getCommitIdentifier();
// If this is the first copy of this row we've seen, mark it as seen and
// move on.
if (empty($seen[$cursor_id][$hash])) {
$seen[$cursor_id][$hash] = true;
continue;
}
// Otherwise, get rid of this row as it duplicates a row we saw previously.
$position->delete();
}
queryfx(
$conn,
'ALTER TABLE %T ADD UNIQUE KEY %T (cursorID, commitIdentifier)',
$table->getTableName(),
$key_name);
queryfx(
$conn,
'UNLOCK TABLES');

View file

@ -3863,6 +3863,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryRefCursorPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRefCursorPHIDType.php',
'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php',
'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php',
'PhabricatorRepositoryRefPosition' => 'applications/repository/storage/PhabricatorRepositoryRefPosition.php',
'PhabricatorRepositoryRepositoryPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php',
'PhabricatorRepositorySchemaSpec' => 'applications/repository/storage/PhabricatorRepositorySchemaSpec.php',
'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.php',
@ -9434,6 +9435,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryRefCursorPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryRefCursorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryRefPosition' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryRepositoryPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositorySchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine',

View file

@ -71,8 +71,11 @@ final class PhabricatorEmailLoginController
$target_email->getUserPHID());
if ($verified_addresses) {
$errors[] = pht(
'That email address is not verified. You can only send '.
'password reset links to a verified address.');
'That email address is not verified, but the account it is '.
'connected to has at least one other verified address. When an '.
'account has at least one verified address, you can only send '.
'password reset links to one of the verified addresses. Try '.
'a verified address instead.');
$e_email = pht('Unverified');
}
}

View file

@ -107,9 +107,11 @@ final class PhabricatorConfigEditor
return parent::transactionHasEffect($object, $xaction);
}
protected function didApplyTransactions(array $xactions) {
protected function didApplyTransactions($object, array $xactions) {
// Force all the setup checks to run on the next page load.
PhabricatorSetupCheck::deleteSetupCheckCache();
return $xactions;
}
public static function storeNewValue(

View file

@ -8,6 +8,7 @@ final class DifferentialRevisionStatus extends Phobject {
const ACCEPTED = 'accepted';
const PUBLISHED = 'published';
const ABANDONED = 'abandoned';
const DRAFT = 'draft';
private $key;
private $spec = array();
@ -76,6 +77,10 @@ final class DifferentialRevisionStatus extends Phobject {
return ($this->key === self::CHANGES_PLANNED);
}
public function isDraft() {
return ($this->key === self::DRAFT);
}
public static function newForStatus($status) {
$result = new self();
@ -163,6 +168,16 @@ final class DifferentialRevisionStatus extends Phobject {
'color.tag' => 'indigo',
'color.ansi' => null,
),
self::DRAFT => array(
'name' => pht('Draft'),
// For legacy clients, treat this as though it is "Needs Review".
'legacy' => 0,
'icon' => 'fa-file-text-o',
'closed' => false,
'color.icon' => 'grey',
'color.tag' => 'grey',
'color.ansi' => null,
),
);
}

View file

@ -137,19 +137,9 @@ final class DifferentialRevisionOperationController
return null;
}
// NOTE: See PHI68. This is a workaround to make "Land Revision" work
// until T11823 is fixed properly. If we find multiple refs with the same
// name (normally, duplicate "master" refs), just pick the first one.
$refs = $this->newRefQuery($repository)
return $this->newRefQuery($repository)
->withRefNames(array($default_name))
->execute();
if ($refs) {
return head($refs);
}
return null;
->executeOne();
}
private function getDefaultRefName(

View file

@ -24,7 +24,7 @@ final class DifferentialChangesSinceLastUpdateField
PhabricatorApplicationTransactionEditor $editor,
array $xactions) {
if ($editor->getIsNewObject()) {
if ($editor->isFirstBroadcast()) {
return;
}

View file

@ -67,7 +67,7 @@ final class DifferentialSummaryField
PhabricatorApplicationTransactionEditor $editor,
array $xactions) {
if (!$editor->getIsNewObject()) {
if (!$editor->isFirstBroadcast()) {
return;
}

View file

@ -71,7 +71,7 @@ final class DifferentialTestPlanField
PhabricatorApplicationTransactionEditor $editor,
array $xactions) {
if (!$editor->getIsNewObject()) {
if (!$editor->isFirstBroadcast()) {
return;
}

View file

@ -26,6 +26,10 @@ final class DifferentialTransactionEditor
return pht('%s created %s.', $author, $object);
}
public function isFirstBroadcast() {
return $this->getIsNewObject();
}
public function getDiffUpdateTransaction(array $xactions) {
$type_update = DifferentialTransaction::TYPE_UPDATE;
@ -600,24 +604,25 @@ final class DifferentialTransactionEditor
return array_values(array_merge($head, $tail));
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {}
return parent::requireCapabilities($object, $xaction);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$object->shouldBroadcast()) {
return false;
}
return true;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$object->shouldBroadcast()) {
return false;
}
return true;
}
@ -633,14 +638,25 @@ final class DifferentialTransactionEditor
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
$action = parent::getMailAction($object, $xactions);
$strongest = $this->getStrongestAction($object, $xactions);
switch ($strongest->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$count = new PhutilNumber($object->getLineCount());
$action = pht('%s, %s line(s)', $action, $count);
break;
$show_lines = false;
if ($this->isFirstBroadcast()) {
$action = pht('Request');
$show_lines = true;
} else {
$action = parent::getMailAction($object, $xactions);
$strongest = $this->getStrongestAction($object, $xactions);
$type_update = DifferentialTransaction::TYPE_UPDATE;
if ($strongest->getTransactionType() == $type_update) {
$show_lines = true;
}
}
if ($show_lines) {
$count = new PhutilNumber($object->getLineCount());
$action = pht('%s, %s line(s)', $action, $count);
}
return $action;
@ -679,6 +695,16 @@ final class DifferentialTransactionEditor
PhabricatorLiskDAO $object,
array $xactions) {
$viewer = $this->requireActor();
// If this is the first time we're sending mail about this revision, we
// generate mail for all prior transactions, not just whatever is being
// applied now. This gets the "added reviewers" lines and other relevant
// information into the mail.
if ($this->isFirstBroadcast()) {
$xactions = $this->loadUnbroadcastTransactions($object);
}
$body = new PhabricatorMetaMTAMailBody();
$body->setViewer($this->requireActor());
@ -1491,4 +1517,113 @@ final class DifferentialTransactionEditor
$acting_phid);
}
private function loadUnbroadcastTransactions($object) {
$viewer = $this->requireActor();
$xactions = id(new DifferentialTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->execute();
return array_reverse($xactions);
}
protected function didApplyTransactions($object, array $xactions) {
// If a draft revision has no outstanding builds and we're automatically
// making drafts public after builds finish, make the revision public.
$auto_undraft = true;
if ($object->isDraft() && $auto_undraft) {
$active_builds = $this->hasActiveBuilds($object);
if (!$active_builds) {
$xaction = $object->getApplicationTransactionTemplate()
->setTransactionType(
DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE)
->setOldValue(false)
->setNewValue(true);
$xaction = $this->populateTransaction($object, $xaction);
// If we're creating this revision and immediately moving it out of
// the draft state, mark this as a create transaction so it gets
// hidden in the timeline and mail, since it isn't interesting: it
// is as though the draft phase never happened.
if ($this->getIsNewObject()) {
$xaction->setIsCreateTransaction(true);
}
$object->openTransaction();
$object
->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW)
->save();
$xaction->save();
$object->saveTransaction();
$xactions[] = $xaction;
}
}
return $xactions;
}
private function hasActiveBuilds($object) {
$viewer = $this->requireActor();
$diff = $object->getActiveDiff();
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withContainerPHIDs(array($object->getPHID()))
->withBuildablePHIDs(array($diff->getPHID()))
->withManualBuildables(false)
->execute();
if (!$buildables) {
return false;
}
$builds = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs(mpull($buildables, 'getPHID'))
->withBuildStatuses(
array(
HarbormasterBuildStatus::STATUS_INACTIVE,
HarbormasterBuildStatus::STATUS_PENDING,
HarbormasterBuildStatus::STATUS_BUILDING,
HarbormasterBuildStatus::STATUS_FAILED,
HarbormasterBuildStatus::STATUS_ABORTED,
HarbormasterBuildStatus::STATUS_ERROR,
HarbormasterBuildStatus::STATUS_PAUSED,
HarbormasterBuildStatus::STATUS_DEADLOCKED,
))
->needBuildTargets(true)
->execute();
if (!$builds) {
return false;
}
$active = array();
foreach ($builds as $key => $build) {
foreach ($build->getBuildTargets() as $target) {
if ($target->isAutotarget()) {
// Ignore autotargets when looking for active of failed builds. If
// local tests fail and you continue anyway, you don't need to
// double-confirm them.
continue;
}
// This build has at least one real target that's doing something.
$active[$key] = $build;
break;
}
}
if (!$active) {
return false;
}
return true;
}
}

View file

@ -37,6 +37,9 @@ final class DifferentialRevisionRequiredActionResultBucket
// other project or package reviewers which they have authority over.
$this->filterResigned($phids);
// We also throw away draft revisions which you aren't the author of.
$this->filterOtherDrafts($phids);
$groups = array();
$groups[] = $this->newGroup()
@ -61,6 +64,11 @@ final class DifferentialRevisionRequiredActionResultBucket
->setNoDataString(pht('No revisions are waiting for updates.'))
->setObjects($this->filterShouldUpdate($phids));
$groups[] = $this->newGroup()
->setName(pht('Drafts'))
->setNoDataString(pht('You have no draft revisions.'))
->setObjects($this->filterDrafts($phids));
$groups[] = $this->newGroup()
->setName(pht('Waiting on Review'))
->setNoDataString(pht('None of your revisions are waiting on review.'))
@ -247,4 +255,36 @@ final class DifferentialRevisionRequiredActionResultBucket
return $results;
}
private function filterOtherDrafts(array $phids) {
$objects = $this->getRevisionsNotAuthored($this->objects, $phids);
$results = array();
foreach ($objects as $key => $object) {
if (!$object->isDraft()) {
continue;
}
$results[$key] = $object;
unset($this->objects[$key]);
}
return $results;
}
private function filterDrafts(array $phids) {
$objects = $this->getRevisionsAuthored($this->objects, $phids);
$results = array();
foreach ($objects as $key => $object) {
if (!$object->isDraft()) {
continue;
}
$results[$key] = $object;
unset($this->objects[$key]);
}
return $results;
}
}

View file

@ -51,8 +51,11 @@ final class DifferentialModernHunk extends DifferentialHunk {
$this->dataEncoding = $this->detectEncodingForStorage($text);
$this->dataType = self::DATATYPE_TEXT;
$this->dataFormat = self::DATAFORMAT_RAW;
$this->data = $text;
list($format, $data) = $this->formatDataForStorage($text);
$this->dataFormat = $format;
$this->data = $data;
return $this;
}
@ -68,24 +71,13 @@ final class DifferentialModernHunk extends DifferentialHunk {
return $this;
}
public function save() {
$type = $this->getDataType();
$format = $this->getDataFormat();
// Before saving the data, attempt to compress it.
if ($type == self::DATATYPE_TEXT) {
if ($format == self::DATAFORMAT_RAW) {
$data = $this->getData();
$deflated = PhabricatorCaches::maybeDeflateData($data);
if ($deflated !== null) {
$this->data = $deflated;
$this->dataFormat = self::DATAFORMAT_DEFLATED;
}
}
private function formatDataForStorage($data) {
$deflated = PhabricatorCaches::maybeDeflateData($data);
if ($deflated !== null) {
return array(self::DATAFORMAT_DEFLATED, $deflated);
}
return parent::save();
return array(self::DATAFORMAT_RAW, $data);
}
public function saveAsText() {
@ -99,7 +91,10 @@ final class DifferentialModernHunk extends DifferentialHunk {
$raw_data = $this->getRawData();
$this->setDataType(self::DATATYPE_TEXT);
$this->setData($raw_data);
list($format, $data) = $this->formatDataForStorage($raw_data);
$this->setDataFormat($format);
$this->setData($data);
$result = $this->save();
@ -118,8 +113,11 @@ final class DifferentialModernHunk extends DifferentialHunk {
$raw_data = $this->getRawData();
list($format, $data) = $this->formatDataForStorage($raw_data);
$this->setDataFormat($format);
$file = PhabricatorFile::newFromFileData(
$raw_data,
$data,
array(
'name' => 'differential-hunk',
'mime-type' => 'application/octet-stream',

View file

@ -653,6 +653,10 @@ final class DifferentialRevision extends DifferentialDAO
return $this->getStatusObject()->isPublished();
}
public function isDraft() {
return $this->getStatusObject()->isDraft();
}
public function getStatusIcon() {
return $this->getStatusObject()->getIcon();
}
@ -690,6 +694,14 @@ final class DifferentialRevision extends DifferentialDAO
return $this;
}
public function shouldBroadcast() {
if (!$this->isDraft()) {
return true;
}
return false;
}
/* -( HarbormasterBuildableInterface )------------------------------------- */

View file

@ -10,7 +10,8 @@ final class DifferentialRevisionAbandonTransaction
return pht('Abandon Revision');
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be abandoned and closed.');
}

View file

@ -10,7 +10,8 @@ final class DifferentialRevisionAcceptTransaction
return pht("Accept Revision \xE2\x9C\x94");
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('These changes will be approved.');
}

View file

@ -52,7 +52,8 @@ abstract class DifferentialRevisionActionTransaction
return DifferentialRevisionEditEngine::ACTIONGROUP_REVISION;
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return null;
}
@ -103,7 +104,7 @@ abstract class DifferentialRevisionActionTransaction
if ($label !== null) {
$field->setCommentActionLabel($label);
$description = $this->getRevisionActionDescription();
$description = $this->getRevisionActionDescription($revision);
$field->setActionDescription($description);
$group_key = $this->getRevisionActionGroupKey();

View file

@ -10,7 +10,8 @@ final class DifferentialRevisionCloseTransaction
return pht('Close Revision');
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be closed.');
}

View file

@ -10,7 +10,8 @@ final class DifferentialRevisionCommandeerTransaction
return pht('Commandeer Revision');
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('You will take control of this revision and become its author.');
}
@ -65,6 +66,11 @@ final class DifferentialRevisionCommandeerTransaction
'been closed. You can only commandeer open revisions.'));
}
if ($object->isDraft()) {
throw new Exception(
pht('You can not commandeer a draft revision.'));
}
if ($this->isViewerRevisionAuthor($object, $viewer)) {
throw new Exception(
pht(

View file

@ -10,7 +10,8 @@ final class DifferentialRevisionPlanChangesTransaction
return pht('Plan Changes');
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht(
'This revision will be removed from review queues until it is revised.');
}
@ -55,6 +56,11 @@ final class DifferentialRevisionPlanChangesTransaction
}
protected function validateAction($object, PhabricatorUser $viewer) {
if ($object->isDraft()) {
throw new Exception(
pht('You can not plan changes to a draft revision.'));
}
if ($object->isChangePlanned()) {
throw new Exception(
pht(

View file

@ -10,7 +10,8 @@ final class DifferentialRevisionReclaimTransaction
return pht('Reclaim Revision');
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be reclaimed and reopened.');
}

View file

@ -10,7 +10,8 @@ final class DifferentialRevisionRejectTransaction
return pht("Request Changes \xE2\x9C\x98");
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be returned to the author for updates.');
}
@ -72,6 +73,11 @@ final class DifferentialRevisionRejectTransaction
'not own.'));
}
if ($object->isDraft()) {
throw new Exception(
pht('You can not request changes to a draft revision.'));
}
if ($this->isViewerFullyRejected($object, $viewer)) {
throw new Exception(
pht(

View file

@ -10,7 +10,8 @@ final class DifferentialRevisionReopenTransaction
return pht('Reopen Revision');
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be reopened for review.');
}

View file

@ -10,8 +10,13 @@ final class DifferentialRevisionRequestReviewTransaction
return pht('Request Review');
}
protected function getRevisionActionDescription() {
return pht('This revision will be returned to reviewers for feedback.');
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
if ($revision->isDraft()) {
return pht('This revision will be submitted to reviewers for feedback.');
} else {
return pht('This revision will be returned to reviewers for feedback.');
}
}
public function getColor() {

View file

@ -10,7 +10,8 @@ final class DifferentialRevisionResignTransaction
return pht('Resign as Reviewer');
}
protected function getRevisionActionDescription() {
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('You will resign as a reviewer for this change.');
}
@ -63,6 +64,11 @@ final class DifferentialRevisionResignTransaction
'been closed. You can only resign from open revisions.'));
}
if ($object->isDraft()) {
throw new Exception(
pht('You can not resign from a draft revision.'));
}
$resigned = DifferentialReviewerStatus::STATUS_RESIGNED;
if ($this->getViewerReviewerStatus($object, $viewer) == $resigned) {
throw new Exception(

View file

@ -106,9 +106,11 @@ final class DiffusionCachedResolveRefsQuery
$cursors = queryfx_all(
$conn_r,
'SELECT refNameHash, refType, commitIdentifier, isClosed FROM %T
WHERE repositoryPHID = %s AND refNameHash IN (%Ls)',
'SELECT c.refNameHash, c.refType, p.commitIdentifier, p.isClosed
FROM %T c JOIN %T p ON p.cursorID = c.id
WHERE c.repositoryPHID = %s AND c.refNameHash IN (%Ls)',
id(new PhabricatorRepositoryRefCursor())->getTableName(),
id(new PhabricatorRepositoryRefPosition())->getTableName(),
$repository->getPHID(),
array_keys($name_hashes));

View file

@ -7,17 +7,18 @@
final class PhabricatorRepositoryRefEngine
extends PhabricatorRepositoryEngine {
private $newRefs = array();
private $deadRefs = array();
private $newPositions = array();
private $deadPositions = array();
private $closeCommits = array();
private $hasNoCursors;
public function updateRefs() {
$this->newRefs = array();
$this->deadRefs = array();
$this->newPositions = array();
$this->deadPositions = array();
$this->closeCommits = array();
$repository = $this->getRepository();
$viewer = $this->getViewer();
$branches_may_close = false;
@ -53,8 +54,9 @@ final class PhabricatorRepositoryRefEngine
);
$all_cursors = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->needPositions(true)
->execute();
$cursor_groups = mgroup($all_cursors, 'getRefType');
@ -63,8 +65,15 @@ final class PhabricatorRepositoryRefEngine
// Find all the heads of closing refs.
$all_closing_heads = array();
foreach ($all_cursors as $cursor) {
if ($this->shouldCloseRef($cursor->getRefType(), $cursor->getRefName())) {
$all_closing_heads[] = $cursor->getCommitIdentifier();
$should_close = $this->shouldCloseRef(
$cursor->getRefType(),
$cursor->getRefName());
if (!$should_close) {
continue;
}
foreach ($cursor->getPositionIdentifiers() as $identifier) {
$all_closing_heads[] = $identifier;
}
}
$all_closing_heads = array_unique($all_closing_heads);
@ -79,25 +88,13 @@ final class PhabricatorRepositoryRefEngine
$this->setCloseFlagOnCommits($this->closeCommits);
}
if ($this->newRefs || $this->deadRefs) {
if ($this->newPositions || $this->deadPositions) {
$repository->openTransaction();
foreach ($this->newRefs as $ref) {
$ref->save();
}
foreach ($this->deadRefs as $ref) {
// Shove this ref into the old refs table so the discovery engine
// can check if any commits have been rendered unreachable.
id(new PhabricatorRepositoryOldRef())
->setRepositoryPHID($repository->getPHID())
->setCommitIdentifier($ref->getCommitIdentifier())
->save();
$ref->delete();
}
$this->saveNewPositions();
$this->deleteDeadPositions();
$repository->saveTransaction();
$this->newRefs = array();
$this->deadRefs = array();
}
$branches = $maps[PhabricatorRepositoryRefCursor::TYPE_BRANCH];
@ -111,10 +108,12 @@ final class PhabricatorRepositoryRefEngine
array $branches) {
assert_instances_of($branches, 'DiffusionRepositoryRef');
$viewer = $this->getViewer();
$all_cursors = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->needPositions(true)
->execute();
$state_map = array();
@ -124,36 +123,57 @@ final class PhabricatorRepositoryRefEngine
continue;
}
$raw_name = $cursor->getRefNameRaw();
$hash = $cursor->getCommitIdentifier();
$state_map[$raw_name][$hash] = $cursor;
foreach ($cursor->getPositions() as $position) {
$hash = $position->getCommitIdentifier();
$state_map[$raw_name][$hash] = $position;
}
}
$updates = array();
foreach ($branches as $branch) {
$cursor = idx($state_map, $branch->getShortName(), array());
$cursor = idx($cursor, $branch->getCommitIdentifier());
if (!$cursor) {
$position = idx($state_map, $branch->getShortName(), array());
$position = idx($position, $branch->getCommitIdentifier());
if (!$position) {
continue;
}
$fields = $branch->getRawFields();
$cursor_state = (bool)$cursor->getIsClosed();
$position_state = (bool)$position->getIsClosed();
$branch_state = (bool)idx($fields, 'closed');
if ($cursor_state != $branch_state) {
$cursor->setIsClosed((int)$branch_state)->save();
if ($position_state != $branch_state) {
$updates[$position->getID()] = (int)$branch_state;
}
}
if ($updates) {
$position_table = id(new PhabricatorRepositoryRefPosition());
$conn = $position_table->establishConnection('w');
$position_table->openTransaction();
foreach ($updates as $position_id => $branch_state) {
queryfx(
$conn,
'UPDATE %T SET isClosed = %d WHERE id = %d',
$position_table->getTableName(),
$branch_state,
$position_id);
}
$position_table->saveTransaction();
}
}
private function markRefNew(PhabricatorRepositoryRefCursor $cursor) {
$this->newRefs[] = $cursor;
private function markPositionNew(
PhabricatorRepositoryRefPosition $position) {
$this->newPositions[] = $position;
return $this;
}
private function markRefDead(PhabricatorRepositoryRefCursor $cursor) {
$this->deadRefs[] = $cursor;
private function markPositionDead(
PhabricatorRepositoryRefPosition $position) {
$this->deadPositions[] = $position;
return $this;
}
@ -203,10 +223,7 @@ final class PhabricatorRepositoryRefEngine
// NOTE: Mercurial branches may have multiple branch heads; this logic
// is complex primarily to account for that.
// Group all the cursors by their ref name, like "master". Since Mercurial
// branches may have multiple heads, there could be several cursors with
// the same name.
$cursor_groups = mgroup($cursors, 'getRefNameRaw');
$cursors = mpull($cursors, null, 'getRefNameRaw');
// Group all the new ref values by their name. As above, these groups may
// have multiple members in Mercurial.
@ -215,38 +232,47 @@ final class PhabricatorRepositoryRefEngine
foreach ($ref_groups as $name => $refs) {
$new_commits = mpull($refs, 'getCommitIdentifier', 'getCommitIdentifier');
$ref_cursors = idx($cursor_groups, $name, array());
$old_commits = mpull($ref_cursors, null, 'getCommitIdentifier');
$ref_cursor = idx($cursors, $name);
if ($ref_cursor) {
$old_positions = $ref_cursor->getPositions();
} else {
$old_positions = array();
}
// We're going to delete all the cursors pointing at commits which are
// no longer associated with the refs. This primarily makes the Mercurial
// multiple head case easier, and means that when we update a ref we
// delete the old one and write a new one.
foreach ($ref_cursors as $cursor) {
if (isset($new_commits[$cursor->getCommitIdentifier()])) {
foreach ($old_positions as $old_position) {
$hash = $old_position->getCommitIdentifier();
if (isset($new_commits[$hash])) {
// This ref previously pointed at this commit, and still does.
$this->log(
pht(
'Ref %s "%s" still points at %s.',
$ref_type,
$name,
$cursor->getCommitIdentifier()));
} else {
// This ref previously pointed at this commit, but no longer does.
$this->log(
pht(
'Ref %s "%s" no longer points at %s.',
$ref_type,
$name,
$cursor->getCommitIdentifier()));
// Nuke the obsolete cursor.
$this->markRefDead($cursor);
$hash));
continue;
}
// This ref previously pointed at this commit, but no longer does.
$this->log(
pht(
'Ref %s "%s" no longer points at %s.',
$ref_type,
$name,
$hash));
// Nuke the obsolete cursor.
$this->markPositionDead($old_position);
}
// Now, we're going to insert new cursors for all the commits which are
// associated with this ref that don't currently have cursors.
$old_commits = mpull($old_positions, 'getCommitIdentifier');
$old_commits = array_fuse($old_commits);
$added_commits = array_diff_key($new_commits, $old_commits);
foreach ($added_commits as $identifier) {
$this->log(
@ -255,12 +281,24 @@ final class PhabricatorRepositoryRefEngine
$ref_type,
$name,
$identifier));
$this->markRefNew(
id(new PhabricatorRepositoryRefCursor())
->setRepositoryPHID($repository->getPHID())
->setRefType($ref_type)
->setRefName($name)
->setCommitIdentifier($identifier));
if (!$ref_cursor) {
// If this is the first time we've seen a particular ref (for
// example, a new branch) we need to insert a RefCursor record
// for it before we can insert a RefPosition.
$ref_cursor = $this->newRefCursor(
$repository,
$ref_type,
$name);
}
$new_position = id(new PhabricatorRepositoryRefPosition())
->setCursorID($ref_cursor->getID())
->setCommitIdentifier($identifier)
->setIsClosed(0);
$this->markPositionNew($new_position);
}
if ($this->shouldCloseRef($ref_type, $name)) {
@ -277,16 +315,21 @@ final class PhabricatorRepositoryRefEngine
// Find any cursors for refs which no longer exist. This happens when a
// branch, tag or bookmark is deleted.
foreach ($cursor_groups as $name => $cursor_group) {
if (idx($ref_groups, $name) === null) {
foreach ($cursor_group as $cursor) {
$this->log(
pht(
'Ref %s "%s" no longer exists.',
$cursor->getRefType(),
$cursor->getRefName()));
$this->markRefDead($cursor);
}
foreach ($cursors as $name => $cursor) {
if (!empty($ref_groups[$name])) {
// This ref still has some positions, so we don't need to wipe it
// out. Try the next one.
continue;
}
foreach ($cursor->getPositions() as $position) {
$this->log(
pht(
'Ref %s "%s" no longer exists.',
$cursor->getRefType(),
$cursor->getRefName()));
$this->markPositionDead($position);
}
}
}
@ -452,6 +495,81 @@ final class PhabricatorRepositoryRefEngine
return $this;
}
private function newRefCursor(
PhabricatorRepository $repository,
$ref_type,
$ref_name) {
$cursor = id(new PhabricatorRepositoryRefCursor())
->setRepositoryPHID($repository->getPHID())
->setRefType($ref_type)
->setRefName($ref_name);
try {
return $cursor->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// If we raced another daemon to create this position and lost the race,
// load the cursor the other daemon created instead.
}
$viewer = $this->getViewer();
$cursor = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withRefTypes(array($ref_type))
->withRefNames(array($ref_name))
->needPositions(true)
->executeOne();
if (!$cursor) {
throw new Exception(
pht(
'Failed to create a new ref cursor (for "%s", of type "%s", in '.
'repository "%s") because it collided with an existing cursor, '.
'but then failed to load that cursor.',
$ref_name,
$ref_type,
$repository->getDisplayName()));
}
return $cursor;
}
private function saveNewPositions() {
$positions = $this->newPositions;
foreach ($positions as $position) {
try {
$position->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// We may race another daemon to create this position. If we do, and
// we lose the race, that's fine: the other daemon did our work for
// us and we can continue.
}
}
$this->newPositions = array();
}
private function deleteDeadPositions() {
$positions = $this->deadPositions;
$repository = $this->getRepository();
foreach ($positions as $position) {
// Shove this ref into the old refs table so the discovery engine
// can check if any commits have been rendered unreachable.
id(new PhabricatorRepositoryOldRef())
->setRepositoryPHID($repository->getPHID())
->setCommitIdentifier($position->getCommitIdentifier())
->save();
$position->delete();
}
$this->deadPositions = array();
}
/* -( Updating Git Refs )-------------------------------------------------- */

View file

@ -54,6 +54,7 @@ final class PhabricatorRepositoryManagementParentsWorkflow
->setViewer($this->getViewer())
->withRefTypes(array(PhabricatorRepositoryRefCursor::TYPE_BRANCH))
->withRepositoryPHIDs(array($repo->getPHID()))
->needPositions(true)
->execute();
$graph = array();
@ -66,23 +67,23 @@ final class PhabricatorRepositoryManagementParentsWorkflow
"%s\n",
pht('Rebuilding branch "%s"...', $ref->getRefName()));
$commit = $ref->getCommitIdentifier();
if ($repo->isGit()) {
$stream = new PhabricatorGitGraphStream($repo, $commit);
} else {
$stream = new PhabricatorMercurialGraphStream($repo, $commit);
}
$discover = array($commit);
while ($discover) {
$target = array_pop($discover);
if (isset($graph[$target])) {
continue;
foreach ($ref->getPositionIdentifiers() as $commit) {
if ($repo->isGit()) {
$stream = new PhabricatorGitGraphStream($repo, $commit);
} else {
$stream = new PhabricatorMercurialGraphStream($repo, $commit);
}
$graph[$target] = $stream->getParents($target);
foreach ($graph[$target] as $parent) {
$discover[] = $parent;
$discover = array($commit);
while ($discover) {
$target = array_pop($discover);
if (isset($graph[$target])) {
continue;
}
$graph[$target] = $stream->getParents($target);
foreach ($graph[$target] as $parent) {
$discover[] = $parent;
}
}
}
}

View file

@ -9,6 +9,7 @@ final class PhabricatorRepositoryRefCursorQuery
private $refTypes;
private $refNames;
private $datasourceQuery;
private $needPositions;
public function withIDs(array $ids) {
$this->ids = $ids;
@ -40,6 +41,11 @@ final class PhabricatorRepositoryRefCursorQuery
return $this;
}
public function needPositions($need) {
$this->needPositions = $need;
return $this;
}
public function newResultObject() {
return new PhabricatorRepositoryRefCursor();
}
@ -68,6 +74,22 @@ final class PhabricatorRepositoryRefCursorQuery
$ref->attachRepository($repository);
}
if (!$refs) {
return $refs;
}
if ($this->needPositions) {
$positions = id(new PhabricatorRepositoryRefPosition())->loadAllWhere(
'cursorID IN (%Ld)',
mpull($refs, 'getID'));
$positions = mgroup($positions, 'getCursorID');
foreach ($refs as $key => $ref) {
$ref_positions = idx($positions, $ref->getID(), array());
$ref->attachPositions($ref_positions);
}
}
return $refs;
}

View file

@ -19,10 +19,9 @@ final class PhabricatorRepositoryRefCursor
protected $refNameHash;
protected $refNameRaw;
protected $refNameEncoding;
protected $commitIdentifier;
protected $isClosed = 0;
private $repository = self::ATTACHABLE;
private $positions = self::ATTACHABLE;
protected function getConfiguration() {
return array(
@ -34,13 +33,12 @@ final class PhabricatorRepositoryRefCursor
self::CONFIG_COLUMN_SCHEMA => array(
'refType' => 'text32',
'refNameHash' => 'bytes12',
'commitIdentifier' => 'text40',
'refNameEncoding' => 'text16?',
'isClosed' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_cursor' => array(
'key_ref' => array(
'columns' => array('repositoryPHID', 'refType', 'refNameHash'),
'unique' => true,
),
),
) + parent::getConfiguration();
@ -74,6 +72,20 @@ final class PhabricatorRepositoryRefCursor
return $this->assertAttached($this->repository);
}
public function attachPositions(array $positions) {
assert_instances_of($positions, 'PhabricatorRepositoryRefPosition');
$this->positions = $positions;
return $this;
}
public function getPositions() {
return $this->assertAttached($this->positions);
}
public function getPositionIdentifiers() {
return mpull($this->getPositions(), 'getCommitIdentifier');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -0,0 +1,26 @@
<?php
final class PhabricatorRepositoryRefPosition
extends PhabricatorRepositoryDAO {
protected $cursorID;
protected $commitIdentifier;
protected $isClosed = 0;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'commitIdentifier' => 'text40',
'isClosed' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_position' => array(
'columns' => array('cursorID', 'commitIdentifier'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
}

View file

@ -56,6 +56,10 @@ final class PhabricatorFulltextToken extends Phobject {
$shade = PHUITagView::COLOR_RED;
$icon = 'fa-minus';
break;
case PhutilSearchQueryCompiler::OPERATOR_SUBSTRING:
$tip = pht('Substring Search');
$shade = PHUITagView::COLOR_VIOLET;
break;
default:
$shade = PHUITagView::COLOR_BLUE;
break;

View file

@ -261,7 +261,7 @@ final class PhabricatorSearchApplicationSearchEngine
foreach ($results as $phid => $handle) {
$view = id(new PhabricatorSearchResultView())
->setHandle($handle)
->setQuery($query)
->setTokens($fulltext_tokens)
->setObject(idx($objects, $phid))
->render();
$list->addItem($view);

View file

@ -3,16 +3,17 @@
final class PhabricatorSearchResultView extends AphrontView {
private $handle;
private $query;
private $object;
private $tokens;
public function setHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle;
return $this;
}
public function setQuery(PhabricatorSavedQuery $query) {
$this->query = $query;
public function setTokens(array $tokens) {
assert_instances_of($tokens, 'PhabricatorFulltextToken');
$this->tokens = $tokens;
return $this;
}
@ -56,88 +57,129 @@ final class PhabricatorSearchResultView extends AphrontView {
* matched their query.
*/
private function emboldenQuery($str) {
$query = $this->query->getParameter('query');
$tokens = $this->tokens;
if (!strlen($query) || !strlen($str)) {
if (!$tokens) {
return $str;
}
// This algorithm is safe but not especially fast, so don't bother if
// we're dealing with a lot of data. This mostly prevents silly/malicious
// queries from doing anything bad.
if (strlen($query) + strlen($str) > 2048) {
if (count($tokens) > 16) {
return $str;
}
// Keep track of which characters we're going to make bold. This is
// byte oriented, but we'll make sure we don't put a bold in the middle
// of a character later.
$bold = array_fill(0, strlen($str), false);
if (!strlen($str)) {
return $str;
}
// Split the query into words.
$parts = preg_split('/ +/', $query);
if (strlen($str) > 2048) {
return $str;
}
// Find all occurrences of each word, and mark them to be emboldened.
foreach ($parts as $part) {
$part = trim($part);
$part = trim($part, '"+');
if (!strlen($part)) {
continue;
$patterns = array();
foreach ($tokens as $token) {
$raw_token = $token->getToken();
$operator = $raw_token->getOperator();
$value = $raw_token->getValue();
switch ($operator) {
case PhutilSearchQueryCompiler::OPERATOR_SUBSTRING:
$patterns[] = '(('.preg_quote($value).'))ui';
break;
case PhutilSearchQueryCompiler::OPERATOR_AND:
$patterns[] = '((?<=\W|^)('.preg_quote($value).')(?=\W|\z))ui';
break;
default:
// Don't highlight anything else, particularly "NOT".
break;
}
}
// Find all matches for all query terms in the document title, then reduce
// them to a map from offsets to highlighted sequence lengths. If two terms
// match at the same position, we choose the longer one.
$all_matches = array();
foreach ($patterns as $pattern) {
$matches = null;
$has_matches = preg_match_all(
'/(?:^|\b)('.preg_quote($part, '/').')/i',
$ok = preg_match_all(
$pattern,
$str,
$matches,
PREG_OFFSET_CAPTURE);
if (!$has_matches) {
if (!$ok) {
continue;
}
// Flag the matching part of the range for boldening.
foreach ($matches[1] as $match) {
$offset = $match[1];
for ($ii = 0; $ii < strlen($match[0]); $ii++) {
$bold[$offset + $ii] = true;
$match_text = $match[0];
$match_offset = $match[1];
if (!isset($all_matches[$match_offset])) {
$all_matches[$match_offset] = 0;
}
$all_matches[$match_offset] = max(
$all_matches[$match_offset],
strlen($match_text));
}
}
// Split the string into ranges, applying bold styling as required.
$out = array();
$buf = '';
$pos = 0;
$is_bold = false;
// Go through the string one display glyph at a time. If a glyph starts
// on a highlighted byte position, turn on highlighting for the nubmer
// of matching bytes. If a query searches for "e" and the document contains
// an "e" followed by a bunch of combining marks, this will correctly
// highlight the entire glyph.
$parts = array();
$highlight = 0;
$offset = 0;
foreach (phutil_utf8v_combined($str) as $character) {
$length = strlen($character);
// Make sure this is UTF8 because phutil_utf8v() will explode if it isn't.
$str = phutil_utf8ize($str);
foreach (phutil_utf8v($str) as $chr) {
if ($bold[$pos] != $is_bold) {
if (strlen($buf)) {
if ($is_bold) {
$out[] = phutil_tag('strong', array(), $buf);
} else {
$out[] = $buf;
}
$buf = '';
}
$is_bold = !$is_bold;
if (isset($all_matches[$offset])) {
$highlight = $all_matches[$offset];
}
$buf .= $chr;
$pos += strlen($chr);
}
if (strlen($buf)) {
if ($is_bold) {
$out[] = phutil_tag('strong', array(), $buf);
if ($highlight > 0) {
$is_highlighted = true;
$highlight -= $length;
} else {
$out[] = $buf;
$is_highlighted = false;
}
$parts[] = array(
'text' => $character,
'highlighted' => $is_highlighted,
);
$offset += $length;
}
// Combine all the sequences together so we aren't emitting a tag around
// every individual character.
$last = null;
foreach ($parts as $key => $part) {
if ($last !== null) {
if ($part['highlighted'] == $parts[$last]['highlighted']) {
$parts[$last]['text'] .= $part['text'];
unset($parts[$key]);
continue;
}
}
$last = $key;
}
// Finally, add tags.
$result = array();
foreach ($parts as $part) {
if ($part['highlighted']) {
$result[] = phutil_tag('strong', array(), $part['text']);
} else {
$result[] = $part['text'];
}
}
return $out;
return $result;
}
}

View file

@ -1105,7 +1105,7 @@ abstract class PhabricatorApplicationTransactionEditor
$this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs();
}
$this->didApplyTransactions($xactions);
$xactions = $this->didApplyTransactions($object, $xactions);
if ($object instanceof PhabricatorCustomFieldInterface) {
// Maybe this makes more sense to move into the search index itself? For
@ -1234,9 +1234,9 @@ abstract class PhabricatorApplicationTransactionEditor
return $xactions;
}
protected function didApplyTransactions(array $xactions) {
protected function didApplyTransactions($object, array $xactions) {
// Hook for subclasses.
return;
return $xactions;
}

View file

@ -809,7 +809,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
if ($this->supportsFerretEngine()) {
$orders['relevance'] = array(
'vector' => array('rank', 'fulltext-modified', 'id'),
'name' => pht('Relevence'),
'name' => pht('Relevance'),
);
}