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:
commit
7ae4d93043
40 changed files with 816 additions and 222 deletions
6
resources/sql/autopatches/20170914.ref.01.position.sql
Normal file
6
resources/sql/autopatches/20170914.ref.01.position.sql
Normal 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};
|
71
resources/sql/autopatches/20170915.ref.01.migrate.php
Normal file
71
resources/sql/autopatches/20170915.ref.01.migrate.php
Normal 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']);
|
||||
}
|
2
resources/sql/autopatches/20170915.ref.02.drop.id.sql
Normal file
2
resources/sql/autopatches/20170915.ref.02.drop.id.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_repository.repository_refcursor
|
||||
DROP COLUMN commitIdentifier;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_repository.repository_refcursor
|
||||
DROP COLUMN isClosed;
|
2
resources/sql/autopatches/20170915.ref.04.uniq.sql
Normal file
2
resources/sql/autopatches/20170915.ref.04.uniq.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_repository.repository_refcursor
|
||||
ADD UNIQUE KEY `key_ref` (repositoryPHID, refType, refNameHash);
|
52
resources/sql/autopatches/20170918.ref.01.position.php
Normal file
52
resources/sql/autopatches/20170918.ref.01.position.php
Normal 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');
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -24,7 +24,7 @@ final class DifferentialChangesSinceLastUpdateField
|
|||
PhabricatorApplicationTransactionEditor $editor,
|
||||
array $xactions) {
|
||||
|
||||
if ($editor->getIsNewObject()) {
|
||||
if ($editor->isFirstBroadcast()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ final class DifferentialSummaryField
|
|||
PhabricatorApplicationTransactionEditor $editor,
|
||||
array $xactions) {
|
||||
|
||||
if (!$editor->getIsNewObject()) {
|
||||
if (!$editor->isFirstBroadcast()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ final class DifferentialTestPlanField
|
|||
PhabricatorApplicationTransactionEditor $editor,
|
||||
array $xactions) {
|
||||
|
||||
if (!$editor->getIsNewObject()) {
|
||||
if (!$editor->isFirstBroadcast()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 )------------------------------------- */
|
||||
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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 )-------------------------------------------------- */
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 )----------------------------------------- */
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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'),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue