Skip to content

Commit

Permalink
Squash revisions
Browse files Browse the repository at this point in the history
  • Loading branch information
it-spiderman authored and FantasticoFox committed Apr 2, 2024
1 parent 25c62f0 commit 204d534
Show file tree
Hide file tree
Showing 15 changed files with 640 additions and 183 deletions.
28 changes: 22 additions & 6 deletions extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,15 @@
"path": "/data_accounting/delete_revisions",
"class": "DataAccounting\\API\\DeleteRevisionsHandler",
"services": [
"PermissionManager",
"DataAccountingVerificationEngine"
"PermissionManager", "DataAccountingRevisionManipulator"
]
},
{
"method": "POST",
"path": "/data_accounting/squash_revisions",
"class": "DataAccounting\\API\\SquashRevisionsHandler",
"services": [
"PermissionManager", "DataAccountingRevisionManipulator"
]
}
],
Expand Down Expand Up @@ -365,10 +372,12 @@
"da-ui-create-page-picker"
]
},
"ext.DataAccounting.deleteRevisions": {
"ext.DataAccounting.revisionActions": {
"scripts": [
"ui/RevisionDialog.js",
"ui/DeleteRevisionsDialog.js",
"ext.DataAccounting.deleteRevisions.js"
"ui/SquashRevisionsDialog.js",
"ext.DataAccounting.revisionActions.js"
],
"dependencies": [
"oojs-ui",
Expand All @@ -378,10 +387,17 @@
"messages": [
"da-ui-delete-revisions-title",
"da-ui-delete-revisions-delete",
"da-ui-delete-revisions-cancel",
"da-ui-button-cancel",
"da-ui-delete-revisions-number",
"da-ui-delete-revisions-witness-warning",
"da-ui-delete-revisions-notice"
"da-ui-delete-revisions-notice",
"da-ui-squash-revisions-title",
"da-ui-squash-revisions-squash",
"da-ui-squash-revisions-number",
"da-ui-squash-revisions-witness-warning",
"da-ui-squash-revisions-notice",
"da-ui-squash-revisions-no-revisions",
"da-ui-delete-revisions-no-revisions"
]
},
"ext.DataAccounting.api": {
Expand Down
13 changes: 11 additions & 2 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,17 @@
"da-transclusion-hashes-link-create": "Create",
"da-ui-delete-revisions-title": "Delete revisions",
"da-ui-delete-revisions-delete": "Delete",
"da-ui-delete-revisions-cancel": "Cancel",
"da-ui-button-cancel": "Cancel",
"da-ui-delete-revisions-number": "How many previous revisions to delete?",
"da-ui-delete-revisions-witness-warning": "One of the revisions that would be deleted has already been witnessed",
"da-ui-delete-revisions-notice": "Following revisions will be deleted:"
"da-ui-delete-revisions-notice": "Following revisions will be deleted:",
"da-ui-squash-revisions-title": "Squash revisions",
"da-ui-squash-revisions-squash": "Squash",
"da-ui-squash-revisions-number": "How many previous revisions to squash into one?",
"da-ui-squash-revisions-witness-warning": "One of the revisions that would be squashed has already been witnessed",
"da-ui-squash-revisions-notice": "Following revisions will be squashed into one:",
"da-ui-squash-revisions-no-revisions": "At least two revisions are needed for squashing",
"da-ui-delete-revisions-no-revisions": "Only one revision of this page available",
"dataaccounting-squash-revisions-comment": "Squashed {{PLURAL:$1|revision $2|revisions: $2}}"

}
30 changes: 23 additions & 7 deletions includes/API/DeleteRevisionsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

namespace DataAccounting\API;

use DataAccounting\RevisionManipulator;
use DataAccounting\Verification\VerificationEngine;
use MediaWiki\MediaWikiServices;
use Exception;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Rest\HttpException;
use MediaWiki\Rest\LocalizedHttpException;
Expand All @@ -18,8 +19,10 @@ class DeleteRevisionsHandler extends SimpleHandler {

/** @var PermissionManager */
private $permissionManager;
/** @var VerificationEngine */
private $verificationEngine;

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

/** @var User */
private $user;

Expand All @@ -29,10 +32,10 @@ class DeleteRevisionsHandler extends SimpleHandler {
*/
public function __construct(
PermissionManager $permissionManager,
VerificationEngine $verificationEngine
RevisionManipulator $revisionManipulator
) {
$this->permissionManager = $permissionManager;
$this->verificationEngine = $verificationEngine;
$this->revisionManipulator = $revisionManipulator;
$this->user = RequestContext::getMain()->getUser();
}

Expand All @@ -48,11 +51,24 @@ public function run() {
}

$revisionIds = $this->getValidatedBody()['ids'];
$this->verificationEngine->deleteRevisions( $revisionIds );

try {
$this->executeAction( $revisionIds );
} catch ( \Exception $e ) {
throw new HttpException( $e->getMessage(), 500 );
}
return [ 'success' => true ];
}

/**
* @param array $revisionIds
*
* @return void
* @throws Exception
*/
protected function executeAction( array $revisionIds ) {
$this->revisionManipulator->deleteRevisions( $revisionIds );
}

/** @inheritDoc */
public function needsWriteAccess() {
return true;
Expand Down
18 changes: 18 additions & 0 deletions includes/API/SquashRevisionsHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace DataAccounting\API;

use Exception;

class SquashRevisionsHandler extends DeleteRevisionsHandler {

/**
* @param array $revisionIds
*
* @return void
* @throws Exception
*/
protected function executeAction( array $revisionIds ) {
$this->revisionManipulator->squashRevisions( $revisionIds );
}
}
30 changes: 21 additions & 9 deletions includes/Hook/AddDAActions.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,27 @@ public function onSkinTemplateNavigation__Universal( $sktemplate, &$links ): voi
];
$sktemplate->getOutput()->addModules( 'ext.dataAccounting.exportSinglePage' );

$entity = $this->verificationEngine->getLookup()->verificationEntityFromTitle( $sktemplate->getTitle() );
if ( $entity->getDomainId() === $this->verificationEngine->getDomainId() ) {
// Manipulate revisions, allowed only on local domain
$links['actions']['da_delete_revisions'] = [
'id' => 'ca-da-delete-revisions',
'href' => '#',
'text' => 'Delete revisions 🗑️',
];
$sktemplate->getOutput()->addModules( 'ext.DataAccounting.deleteRevisions' );
if ( $this->permissionManager->userCan( 'delete', $sktemplate->getUser(), $sktemplate->getTitle() ) ) {
$entity = $this->verificationEngine->getLookup()->verificationEntityFromTitle( $sktemplate->getTitle() );
if ( !$entity ) {
return;
}
if ( $entity->getDomainId() === $this->verificationEngine->getDomainId() ) {
// Delete revisions, allowed only on local domain
$links['actions']['da_delete_revisions'] = [
'id' => 'ca-da-delete-revisions',
'href' => '#',
'text' => 'Delete revisions 🗑️',
];
// Squash revisions, allowed only on local domain
$links['actions']['da_squash_revisions'] = [
'id' => 'ca-da-squash-revisions',
'href' => '#',
'text' => 'Squash revisions 💥',
];$sktemplate->getOutput()->addModules( 'ext.DataAccounting.revisionActions' );

}
}

}
}
170 changes: 170 additions & 0 deletions includes/RevisionManipulator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

namespace DataAccounting;

use CommentStoreComment;
use DataAccounting\Verification\VerificationEngine;
use Exception;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionStore;
use Message;
use Wikimedia\Rdbms\ILoadBalancer;

class RevisionManipulator {

/** @var ILoadBalancer */
private $lb;
/** @var RevisionStore */
private $revisionStore;
/** @var VerificationEngine */
private $verificationEngine;

/**
* @param ILoadBalancer $lb
* @param RevisionStore $revisionStore
* @param VerificationEngine $verificationEngine
*/
public function __construct(
ILoadBalancer $lb, RevisionStore $revisionStore, VerificationEngine $verificationEngine
) {
$this->lb = $lb;
$this->revisionStore = $revisionStore;
$this->verificationEngine = $verificationEngine;
}

/**
* @param array $revisionIds
*
* @return void
* @throws Exception
*/
public function deleteRevisions( array $revisionIds ) {
$this->assertRevisionsOfSamePage( $revisionIds );
$firstToDelete = min( $revisionIds );
$firstToDelete = $this->revisionStore->getRevisionById( $firstToDelete );
$nowLatest = $this->revisionStore->getPreviousRevision( $firstToDelete );
if ( !$nowLatest ) {
throw new Exception( 'After deleting requested revisions, no revision remains' );
}
$this->rawDeleteRevisions( $revisionIds );
$dbw = $this->lb->getConnection( DB_PRIMARY );
$dbw->update(
'page',
[ 'page_latest' => $nowLatest->getId() ],
[ 'page_id' => $firstToDelete->getPageId() ],
__METHOD__
);

foreach ( $revisionIds as $revisionId ) {
$this->verificationEngine->getLookup()->deleteForRevId( $revisionId );
}
$this->verificationEngine->buildAndUpdateVerificationData(
$this->verificationEngine->getLookup()->verificationEntityFromRevId( $nowLatest->getId() ),
$nowLatest
);
}

public function squashRevisions( array $revisionIds ) {
$this->assertRevisionsOfSamePage( $revisionIds );
// 1. Get the content of the latest revisions
$latest = max( $revisionIds );
$latest = $this->revisionStore->getRevisionById( $latest );
if ( !$latest->isCurrent() ) {
throw new Exception( 'Latest requested revision is not current' );
}

$firstToDelete = min( $revisionIds );
$firstToDelete = $this->revisionStore->getRevisionById( $firstToDelete );
$lastRemaining = $this->revisionStore->getPreviousRevision( $firstToDelete );

$pageIdentity = $latest->getPage();
$timestamp = $latest->getTimestamp();
$actor = $latest->getUser();
$roles = $latest->getSlotRoles();
$contents = [];
foreach ( $roles as $role ) {
$contents[$role] = $latest->getContent( $role );
}

// 2. Delete all revisions that are about to be merged
$this->rawDeleteRevisions( $revisionIds );

// 3. Insert a new revision with the content of the latest revision
$revRecord = new MutableRevisionRecord( $pageIdentity );
$revRecord->setTimestamp( $timestamp );
$revRecord->setUser( $actor );
if ( $lastRemaining ) {
$revRecord->setParentId( $lastRemaining->getId() );
}
$comment = CommentStoreComment::newUnsavedComment(
Message::newFromKey( 'dataaccounting-squash-revisions-comment' )
->params( count( $revisionIds ), implode( ',', $revisionIds ) )->inContentLanguage()->text()
);
$revRecord->setComment( $comment );
$revRecord->setMinorEdit( false );
$revRecord->setPageId( $pageIdentity->getId() );

foreach ( $contents as $role => $content ) {
$revRecord->setContent( $role, $content );
}

$revRecord = $this->revisionStore->insertRevisionOn( $revRecord, $this->lb->getConnection( DB_PRIMARY ) );
$dbw = $this->lb->getConnection( DB_PRIMARY );

// 4. Set new revision as the latest revision of the page
$dbw->update(
'page',
[ 'page_latest' => $revRecord->getId() ],
[ 'page_id' => $pageIdentity->getId() ],
__METHOD__
);
// 5. Delete verification data for the deleted revisions
foreach ( $revisionIds as $revisionId ) {
$this->verificationEngine->getLookup()->deleteForRevId( $revisionId );
}
// 6. Update verification data for the new revision
$this->verificationEngine->buildAndUpdateVerificationData(
$this->verificationEngine->getLookup()->verificationEntityFromRevId( $revRecord->getId() ),
$revRecord
);
}

/**
* Execute actual deletion
*
* @param array $revisionIds
*
* @return void
*/
protected function rawDeleteRevisions( array $revisionIds ) {
$dbw = $this->lb->getConnection( DB_PRIMARY );
// Delete revisions
$dbw->delete( 'revision', [ 'rev_id' => $revisionIds ], __METHOD__ );
$dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $revisionIds ], __METHOD__ );
}

/**
* @param array $revisionIds
*
* @return void
* @throws Exception
*/
protected function assertRevisionsOfSamePage( array $revisionIds ) {
$pageId = null;
foreach ( $revisionIds as $id ) {
$rev = $this->revisionStore->getRevisionById( $id );
if ( !$rev ) {
throw new Exception( "Revision $id does not exist" );
}
if ( $pageId === null ) {
$pageId = $rev->getPageId();
continue;
}
if ( $pageId !== $rev->getPageId() ) {
throw new Exception( 'Requested revisions are not of the same page' );
}
}
}


}
8 changes: 8 additions & 0 deletions includes/ServiceWiring.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use DataAccounting\Config\Handler;
use DataAccounting\RevisionManipulator;
use DataAccounting\TransclusionManager;
use DataAccounting\Transfer\Exporter;
use DataAccounting\Transfer\Importer;
Expand Down Expand Up @@ -81,4 +82,11 @@
$services->getService( 'DataAccountingVerificationEngine' )
);
},
'DataAccountingRevisionManipulator' => static function( MediaWikiServices $services ): RevisionManipulator {
return new RevisionManipulator(
$services->getDBLoadBalancer(),
$services->getRevisionStore(),
$services->getService( 'DataAccountingVerificationEngine' )
);
}
];
Loading

0 comments on commit 204d534

Please sign in to comment.