Skip to content

Commit

Permalink
Merge logic
Browse files Browse the repository at this point in the history
  • Loading branch information
it-spiderman committed May 15, 2024
1 parent d4e4e84 commit 6a95e95
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 50 deletions.
2 changes: 1 addition & 1 deletion extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
},
"Inbox": {
"class": "DataAccounting\\SpecialInbox",
"services": [ "TitleFactory", "DataAccountingVerificationEngine", "RevisionLookup" ]
"services": [ "TitleFactory", "DataAccountingVerificationEngine", "RevisionLookup", "DataAccountingRevisionManipulator" ]
}
},

Expand Down
26 changes: 24 additions & 2 deletions includes/Inbox/InboxImporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

namespace DataAccounting\Inbox;

use DataAccounting\RevisionManipulator;
use DataAccounting\Verification\Entity\VerificationEntity;
use DataAccounting\Verification\VerificationEngine;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\User\UserIdentity;
use MWException;
use Status;
use Title;
use User;
Expand All @@ -19,13 +22,20 @@ class InboxImporter {
/** @var RevisionLookup */
private $revisionLookup;

/** @var RevisionManipulator */
private $revisionManipulator;

/**
* @param VerificationEngine $verificationEngine
* @param RevisionLookup $revisionLookup
* @param RevisionManipulator $revisionManipulator
*/
public function __construct( VerificationEngine $verificationEngine, RevisionLookup $revisionLookup ) {
public function __construct(
VerificationEngine $verificationEngine, RevisionLookup $revisionLookup, RevisionManipulator $revisionManipulator
) {
$this->verificationEngine = $verificationEngine;
$this->revisionLookup = $revisionLookup;
$this->revisionManipulator = $revisionManipulator;
}

/**
Expand Down Expand Up @@ -78,7 +88,7 @@ public function getTreeBuilder(): TreeBuilder {
/**
* @param Title $title
* @param User $user
* @param string|null $comment
* @param string $comment
*
* @return Status
*/
Expand Down Expand Up @@ -109,4 +119,16 @@ private function doMoveTitle( Title $from, Title $to, User $user, string $commen
return $movePage->move( $user, $comment, false );
}

/**
* @param Title $local
* @param Title $remoteTitle
* @param User $user
* @param string $text
* @return void
* @throws MWException
*/
public function mergePages( Title $local, Title $remoteTitle, User $user, string $text ) {
$this->revisionManipulator->mergePages( $local, $remoteTitle, $user, $text );
$this->doDeleteTitle( $remoteTitle, $user );
}
}
4 changes: 3 additions & 1 deletion includes/Inbox/TreeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ private function combine( array $remote, array $local, Language $language, UserI

$this->decorateWithRevisionData( $combined, $language, $user );
uasort( $combined, static function( array $a, array $b ) {
return $a['revisionData']['timestamp_raw'] <=> $b['revisionData']['timestamp_raw'];
// Sort by revision timestamp descending
return $b['revisionData']['timestamp_raw'] <=> $a['revisionData']['timestamp_raw'];
//return $a['revisionData']['timestamp_raw'] <=> $b['revisionData']['timestamp_raw'];
} );

return [
Expand Down
192 changes: 177 additions & 15 deletions includes/RevisionManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
namespace DataAccounting;

use CommentStoreComment;
use DataAccounting\Verification\Entity\VerificationEntity;
use DataAccounting\Verification\VerificationEngine;
use Exception;
use MediaWiki\Page\WikiPageFactory;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\User\UserIdentity;
use Message;
use MWException;
use Title;
use Wikimedia\Rdbms\ILoadBalancer;
use WikitextContent;

class RevisionManipulator {

Expand Down Expand Up @@ -177,32 +180,174 @@ public function forkPage( Title $source, Title $target, RevisionRecord $maxRev,
* @param Title $target
* @param UserIdentity $user
* @param array $revisionParents
* @return void
* @return VerificationEntity[]
* @throws MWException
*/
private function insertRevisions( array $revisions, Title $target, UserIdentity $user, array $revisionParents ) {
$wp = $this->wikipageFactory->newFromTitle( $target );
private function insertRevisions(
array $revisions, Title $target, UserIdentity $user, array $revisionParents
): array {
$createdEntities = [];
foreach ( $revisions as $revision ) {
$updater = $wp->newPageUpdater( $user );
$roles = $revision->getSlotRoles();
foreach ( $roles as $role ) {
$content = $revision->getContent( $role );
$updater->setContent( $role, $content );
}

$newRev = $updater->saveRevision(
CommentStoreComment::newUnsavedComment( 'Forked from ' . $revision->getId() ),
EDIT_SUPPRESS_RC | EDIT_INTERNAL
);
$newRev = $this->doInsertRevision( $revision, $target, $user );
if ( !$newRev ) {
throw new Exception( 'Failed to save revision' );
}
$parentEntity = $revisionParents[$revision->getId()] ?? null;
$this->verificationEngine->buildAndUpdateVerificationData(
$createdEntities[] = $this->verificationEngine->buildAndUpdateVerificationData(
$this->verificationEngine->getLookup()->verificationEntityFromRevId( $newRev->getId() ),
$newRev, $parentEntity
);
}
// Remove nulls
return array_filter( $createdEntities );
}

/**
* @param RevisionRecord $revision
* @param Title $target
* @param UserIdentity $user
* @return RevisionRecord|null
* @throws MWException
*/
private function doInsertRevision( RevisionRecord $revision, Title $target, UserIdentity $user ): ?RevisionRecord {
$wp = $this->wikipageFactory->newFromTitle( $target );
$updater = $wp->newPageUpdater( $user );
$roles = $revision->getSlotRoles();
foreach ( $roles as $role ) {
$content = $revision->getContent( $role );
$updater->setContent( $role, $content );
}

return $updater->saveRevision(
CommentStoreComment::newUnsavedComment( 'Forked from ' . $revision->getId() ),
EDIT_SUPPRESS_RC | EDIT_INTERNAL
);
}

/**
* @param Title $local
* @param Title $remote
* @param UserIdentity $user
* @param string $mergedText
* @return void
* @throws MWException
*/
public function mergePages( Title $local, Title $remote, UserIdentity $user, string $mergedText ) {
$localEntities = $this->getVerificationEntitiesForMerge( $local );
$remoteEntities = $this->getVerificationEntitiesForMerge( $remote );
$commonParent = $this->getCommonParent( $localEntities, $remoteEntities );
if ( !$commonParent ) {
throw new Exception( 'No common parent found' );
}
$this->assertSameGenesis( $localEntities, $remoteEntities );

$wp = $this->wikipageFactory->newFromTitle( $local );
$lastInserted = null;
foreach ( $remoteEntities as $hash => $remoteEntity ) {
if ( isset( $localEntities[$hash] ) ) {
error_log( "Skipping $hash" );
// Exists locally
continue;
}
error_log( "Inserting $hash" );
$newLocalRev = $this->doInsertRevision( $remoteEntity->getRevision(), $local, $user );
error_log( "CREATED REV: " . ( $newLocalRev ? $newLocalRev->getId() : 'failed' ) );
$remoteEntity = $this->moveVerificationEntity( $remoteEntity, $newLocalRev );
if ( !$remoteEntity ) {
throw new Exception( 'Failed to move verification entity' );
}
error_log( "RELOADED ENTITY" );
$lastInserted = $remoteEntity;
}
// Insert merged revision
$updater = $wp->newPageUpdater( $user );
$updater->setContent( SlotRecord::MAIN, new WikitextContent( $mergedText ) );
$mergedRev = $updater->saveRevision(
CommentStoreComment::newUnsavedComment( 'Merged from ' . $remote->getPrefixedText() )
);
if ( !$mergedRev ) {
throw new Exception( 'Failed to save merged revision' );
}
// Last local revision is the official parent, since remote changes are branched-off
$lastLocal = array_pop( $localEntities );
$this->verificationEngine->buildAndUpdateVerificationData(
$this->verificationEngine->getLookup()->verificationEntityFromRevId( $mergedRev->getId() ),
$mergedRev, $lastLocal, $lastInserted->getHash()
);
}

/**
* @param VerificationEntity $entity
* @param RevisionRecord $newRevision
* @return VerificationEntity|null
* @throws Exception
*/
private function moveVerificationEntity(
VerificationEntity $entity, RevisionRecord $newRevision
): ?VerificationEntity {
$newVE = $this->verificationEngine->getLookup()->verificationEntityFromRevId( $newRevision->getId() );
error_log( "RETRIEVE ENTITY FOR REV: {$newRevision->getId()}" );
if ( !$newVE ) {
error_log( "NOT" );
throw new Exception( 'Failed to get verification entity for new revision' );
}
// Do switching, replace newly inserted VE for new revision with old one,
// but update data so that it points to the new revision
$insertData = [
'page_id' => $newVE->getTitle()->getArticleID(),
'page_title' => $newVE->getTitle()->getPrefixedDBkey()
];
error_log( "UPDATING" );
error_log( var_export( $insertData, 1 ) );
$db = $this->lb->getConnection( DB_PRIMARY );
// Delete new one
error_log( "DELETE NEW ONE" );
$this->verificationEngine->getLookup()->deleteForRevId( $newRevision->getId() );
// Update old VE, so that it points to the new revision
$db->update(
'revision_verification',
$insertData,
[ 'rev_id' => $entity->getRevision()->getId() ],
);
error_log( "QUERY: " . $db->lastQuery() );

// Reload updated VE
return $this->verificationEngine->getLookup()->verificationEntityFromRevId( $newRevision->getId() );
}

/**
* @param Title $title
* @return VerificationEntity[]
*/
private function getVerificationEntitiesForMerge( Title $title ) {
$revs = $this->verificationEngine->getLookup()->getAllRevisionIds( $title );
$entities = [];
foreach ( $revs as $rev ) {
$entity = $this->verificationEngine->getLookup()->verificationEntityFromRevId( $rev );
if ( !$entity ) {
continue;
}
$entities[$entity->getHash()] = $entity;
}

return $entities;
}

/**
* @param array $local
* @param array $remote
* @return void
* @throws Exception
*/
private function assertSameGenesis( array $local, array $remote ) {
if ( empty( $local ) || empty( $remote ) ) {
throw new Exception( 'No revisions found in source or target page' );
}
$localGenesis = $local[array_key_first( $local )]->getHash( VerificationEntity::GENESIS_HASH );
$remoteGenesis = $remote[array_key_first( $remote )]->getHash( VerificationEntity::GENESIS_HASH );
if ( $localGenesis !== $remoteGenesis ) {
throw new Exception( 'Source and target pages have different genesis hashes' );
}
}

/**
Expand Down Expand Up @@ -241,4 +386,21 @@ protected function assertRevisionsOfSamePage( array $revisionIds ) {
}
}
}

/**
* @param array $localEntities
* @param array $remoteEntities
* @return VerificationEntity|null
*/
private function getCommonParent( array $localEntities, array $remoteEntities ): ?VerificationEntity {
$localHashes = array_keys( $localEntities );
$remoteHashes = array_keys( $remoteEntities );
$common = array_intersect( $localHashes, $remoteHashes );
if ( empty( $common ) ) {
return null;
}
$last = array_pop( $common );
return $localEntities[$last];
}

}
Loading

0 comments on commit 6a95e95

Please sign in to comment.