jenkins-bot has submitted this change and it was merged. Change subject: \SMW\ChangeObserver ......................................................................
\SMW\ChangeObserver Code coverage: 100% CRAP: 10 The ChangeObserver acts on an invoked object notification and enforces loose coupling between the sender of a change notification through this agent. ## Narrative Before using the ChangeObserver, PropertyDisparityDetector would have been coupled to the UpdateDispatcher instead now the ChangeAgent acts on behalf of the PropertyDisparityDetector to invoke the necessary object. This will allow [1] to use the same ChangeObserver [1] https://gerrit.wikimedia.org/r/#/c/75825/ ## Testing * MockChangeObserver is being used to verify the Observer/Subject behaviour Change-Id: Ifd8604d8ed551a62e3aa5c160e536c5cb1d0e718 --- M SemanticMediaWiki.settings.php M includes/ParserData.php M includes/PropertyDisparityDetector.php M includes/Settings.php M includes/Setup.php M includes/jobs/PropertySubjectsUpdateDispatcherJob.php A includes/utilities/ChangeObserver.php A includes/utilities/MediaWikiInterfaceProvider.php A tests/phpunit/MockChangeObserver.php M tests/phpunit/includes/PropertyDisparityDetectorTest.php A tests/phpunit/includes/utilities/ChangeObserverTest.php 11 files changed, 543 insertions(+), 41 deletions(-) Approvals: Mwjames: Looks good to me, approved jenkins-bot: Verified diff --git a/SemanticMediaWiki.settings.php b/SemanticMediaWiki.settings.php index 50c9cfe..88bb8ae 100644 --- a/SemanticMediaWiki.settings.php +++ b/SemanticMediaWiki.settings.php @@ -644,3 +644,15 @@ ## $smwgPropertyZeroCountDisplay = true; ## + +### +# Support of deferred update of properties using the JobQueue/CacheStore +# to lighten performance degradation when changing properties +# +# In order to enable deferred updates, it requires the CacheStore +# (e.g smwgCacheType) to be enabled and PropertyPageIdMapper to be present. +# +# @since 1.9 +## +$smwgDeferredPropertyUpdate = false; +## diff --git a/includes/ParserData.php b/includes/ParserData.php index 0acc9c8..5ea128c 100644 --- a/includes/ParserData.php +++ b/includes/ParserData.php @@ -387,8 +387,6 @@ return true; } - $dispatcherJob = null; - $namespace = $this->title->getNamespace(); $wikiPage = WikiPage::factory( $this->title ); $revision = $wikiPage->getRevision(); @@ -422,7 +420,8 @@ // even finding uses of a property fails after its type was changed. if ( $this->updateJobs ) { $disparityDetector = new PropertyDisparityDetector( $store, $this->semanticData, Settings::newFromGlobals() ); - $dispatcherJob = $disparityDetector->detectDisparity()->getDispatcherJob(); + $disparityDetector->attach( new ChangeObserver() ); + $disparityDetector->detectDisparity(); } // Actually store semantic data, or at least clear it if needed @@ -432,14 +431,7 @@ $store->clearData( $this->semanticData->getSubject() ); } - // Job::batchInsert was deprecated in MW 1.21 - // @see JobQueueGroup::singleton()->push( $job ); - if ( $dispatcherJob !== null ) { - Job::batchInsert( $dispatcherJob ); - } - Profiler::Out( __METHOD__, true ); - return true; } diff --git a/includes/PropertyDisparityDetector.php b/includes/PropertyDisparityDetector.php index bed5dc9..e14bf06 100644 --- a/includes/PropertyDisparityDetector.php +++ b/includes/PropertyDisparityDetector.php @@ -2,9 +2,6 @@ namespace SMW; -use Title; -use Job; - /** * Class that detects a disparity between the property object and its store data * @@ -37,7 +34,7 @@ * * @ingroup SMW */ -class PropertyDisparityDetector { +class PropertyDisparityDetector extends Subject implements TitleProvider { /** @var Store */ protected $store; @@ -48,8 +45,8 @@ /** @var Settings */ protected $settings; - /** @var Job */ - protected $dispatcherJob = null; + /** @var boolean */ + protected $hasDisparity = false; /** * @since 1.9 @@ -65,14 +62,14 @@ } /** - * Returns update jobs as a result of the data comparison + * Returns a Title object * * @since 1.9 * - * @return PropertyDisparityDispatcherJob|null + * @return Title */ - public function getDispatcherJob() { - return $this->dispatcherJob; + public function getTitle() { + return $this->semanticData->getSubject()->getTitle(); } /** @@ -83,7 +80,7 @@ * @return boolean */ public function hasDisparity() { - return $this->getDispatcherJob() !== null; + return $this->hasDisparity; } /** @@ -179,11 +176,9 @@ * @param boolean $addJob */ protected function addDispatchJob( $addJob = true ) { - if ( $addJob && $this->dispatcherJob === null ) { - $this->dispatcherJob[] = new PropertySubjectsUpdateDispatcherJob( - $this->semanticData->getSubject()->getTitle(), - array( 'store' => get_class( $this->store ) ) - ); + if ( $addJob && !$this->hasDisparity ) { + $this->setState( 'runUpdateDispatcher' ); + $this->hasDisparity = true; } } diff --git a/includes/Settings.php b/includes/Settings.php index db5c8c6..1621efc 100644 --- a/includes/Settings.php +++ b/includes/Settings.php @@ -136,6 +136,7 @@ 'smwgFixedProperties' => $GLOBALS['smwgFixedProperties'], 'smwgPropertyLowUsageThreshold' => $GLOBALS['smwgPropertyLowUsageThreshold'], 'smwgPropertyZeroCountDisplay' => $GLOBALS['smwgPropertyZeroCountDisplay'], + 'smwgDeferredPropertyUpdate' => $GLOBALS['smwgDeferredPropertyUpdate'], ); if ( self::$instance === null ) { diff --git a/includes/Setup.php b/includes/Setup.php index e8c6123..7fe0d7b 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -166,6 +166,8 @@ $wgAutoloadClasses['SMW\ObservableMessageReporter'] = $incDir . '/utilities/MessageReporter.php'; $wgAutoloadClasses['SMW\RedirectBuilder'] = $incDir . '/utilities/RedirectBuilder.php'; $wgAutoloadClasses['SMW\ParserOutputGenerator'] = $incDir . '/utilities/ParserOutputGenerator.php'; + $wgAutoloadClasses['SMW\ChangeObserver'] = $incDir . '/utilities/ChangeObserver.php'; + $wgAutoloadClasses['SMW\TitleProvider'] = $incDir . '/utilities/MediaWikiInterfaceProvider.php'; $wgAutoloadClasses['SMW\Publisher'] = $incDir . '/utilities/ObserverInterfaceProvider.php'; $wgAutoloadClasses['SMW\Subject'] = $incDir . '/utilities/ObserverInterfaceProvider.php'; @@ -424,6 +426,7 @@ $wgAutoloadClasses['SMW\Test\MockObjectBuilder'] = $testsDir . 'MockObjectBuilder.php'; $wgAutoloadClasses['SMW\Test\SpecialPageTestCase'] = $testsDir . 'SpecialPageTestCase.php'; $wgAutoloadClasses['SMW\Test\CompatibilityTestCase'] = $testsDir . 'CompatibilityTestCase.php'; + $wgAutoloadClasses['SMW\Test\MockChangeObserver'] = $testsDir . 'MockChangeObserver.php'; // Jobs $wgAutoloadClasses['SMW\JobBase'] = $smwgIP . 'includes/jobs/JobBase.php'; diff --git a/includes/jobs/PropertySubjectsUpdateDispatcherJob.php b/includes/jobs/PropertySubjectsUpdateDispatcherJob.php index 284495e..ca969da 100644 --- a/includes/jobs/PropertySubjectsUpdateDispatcherJob.php +++ b/includes/jobs/PropertySubjectsUpdateDispatcherJob.php @@ -174,4 +174,16 @@ } } } + + /** + * @see Job::insert + * + * @since 1.9 + * @codeCoverageIgnore + */ + public function insert() { + if ( $this->getSettings()->get( 'smwgEnableUpdateJobs' ) ) { + parent::insert(); + } + } } diff --git a/includes/utilities/ChangeObserver.php b/includes/utilities/ChangeObserver.php new file mode 100644 index 0000000..25c49fc --- /dev/null +++ b/includes/utilities/ChangeObserver.php @@ -0,0 +1,160 @@ +<?php + +namespace SMW; + +/** + * General purpose change agent to enforce loose coupling by having + * a Publisher (subject) sent a change notification to this observer + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ + +/** + * General purpose change agent to enforce loose coupling by having + * a Publisher (subject) sent a change notification to this observer + * + * @note When testing rountrips, use MockChangeObserver instead + * + * @ingroup Observer + */ +class ChangeObserver extends Observer { + + /** @var Settings */ + protected $settings = null; + + /** @var Store */ + protected $store = null; + + /** @var CacheHandler */ + protected $cache = null; + + /** + * Sets Store object + * + * @since 1.9 + * + * @param Store $store + */ + public function setStore( Store $store ) { + $this->store = $store; + } + + /** + * Returns Store object + * + * @since 1.9 + * + * @return Store + */ + public function getStore() { + + if ( $this->store === null ) { + $this->store = StoreFactory::getStore(); + } + + return $this->store; + } + + /** + * Sets Settings object + * + * @since 1.9 + * + * @param Settings $settings + * + * @return ChangeAgent + */ + public function setSettings( Settings $settings ) { + $this->settings = $settings; + return $this; + } + + /** + * Returns Settings object + * + * @since 1.9 + * + * @return Settings + */ + public function getSettings() { + + if ( $this->settings === null ) { + $this->settings = Settings::newFromGlobals(); + } + + return $this->settings; + } + + /** + * Returns CacheHandler object + * + * @since 1.9 + * + * @return CacheHandler + */ + public function getCache() { + + if ( $this->cache === null ) { + $this->cache = CacheHandler::newFromId( $this->getSettings()->get( 'smwgCacheType' ) ); + } + + return $this->cache; + } + + /** + * UpdateJob dispatching + * + * loading of data + * + * Generally by the time the job is execute the store has been updated and + * data that belong to a property potentially are no longer are associate + * with a subject. + * + * [1] Immediate dispatching influences the performance during page saving + * since data that belongs to the property are loaded directly from the DB + * which has direct impact about the response time of the page in question. + * + * [2] Deferred dispatching uses a different approach to recognize necessary + * objects involved and deferres property/pageIds mapping to the JobQueue. + * This makes it unnecessary to load data from the DB therefore decrease + * performance degration during page update. + * + * @since 1.9 + * + * @param TitleProvider $subject + */ + public function runUpdateDispatcher( TitleProvider $subject ) { + + $dispatcher = new PropertySubjectsUpdateDispatcherJob( $subject->getTitle() ); + $dispatcher->setSettings( $this->getSettings() ); + + if ( $this->getSettings()->get( 'smwgDeferredPropertyUpdate' ) ) { + $dispatcher->insert(); // JobQueue is handling dispatching + } else { + $dispatcher->run(); + } + + return true; + } + +} diff --git a/includes/utilities/MediaWikiInterfaceProvider.php b/includes/utilities/MediaWikiInterfaceProvider.php new file mode 100644 index 0000000..26a931d --- /dev/null +++ b/includes/utilities/MediaWikiInterfaceProvider.php @@ -0,0 +1,47 @@ +<?php + +namespace SMW; + +/** + * Specifies interfaces to access MediaWiki specific objects + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ + +/** + * Interface describing access to a Title object + * + * @ingroup Provider + */ +interface TitleProvider { + + /** + * Returns a Title object + * + * @since 1.9 + * + * @return Title + */ + public function getTitle(); + +} \ No newline at end of file diff --git a/tests/phpunit/MockChangeObserver.php b/tests/phpunit/MockChangeObserver.php new file mode 100644 index 0000000..5d8d379 --- /dev/null +++ b/tests/phpunit/MockChangeObserver.php @@ -0,0 +1,95 @@ +<?php + +namespace SMW\Test; + +use \SMW\ChangeObserver; +use \SMW\TitleProvider; + +/** + * MockChangeAgent should only be used during testing to establish that + * a correct behaviour between Observer and Subject has been established. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ + +/** + * MockChangeObserver should only be used during testing to establish that + * a correct behaviour between Observer and Subject has been established. + * + * Use the setNotifier/getNotifier to verify the function that was expected + * during processing was invoked because of a notification sent by the + * Publisher(Subject). + * + * Testing the publisher does not involve testing the execution of + * the expected processor and is part of a different test which should be + * treated as independent unit test. + * + * @par Example: + * @code + * $observer = new MockChangeObserver(); + * $instance->attach( $observer ); + * + * $this->assertEquals( 'runFoo', $observer->getNotifier() ) + * @end + * + * @ingroup Observer + * @codeCoverageIgnore + */ +class MockChangeObserver extends ChangeObserver { + + /** @var string */ + protected $notifier = null; + + /** + * Sets a notifier + * + * Verfifies that the expected notifier is actually available in the + * parent class, meaning that any unrecognized renaming of any method + * in the mock or parent class will be flagged by returning null + * instead of the expected notifier name. + * + * @since 1.9 + */ + public function setNotifier( $notifier ) { + $this->notifier = method_exists( get_parent_class( $this ), $notifier ) ? $notifier : null; + } + + /** + * Returns a registered notifier + * + * @since 1.9 + */ + public function getNotifier() { + return $this->notifier; + } + + /** + * @see ChangeObserver::runUpdateDispatcher + * + * @since 1.9 + */ + public function runUpdateDispatcher( TitleProvider $subject ) { + $this->setNotifier( __FUNCTION__ ); + } + +} diff --git a/tests/phpunit/includes/PropertyDisparityDetectorTest.php b/tests/phpunit/includes/PropertyDisparityDetectorTest.php index 5bf0b4e..979393b 100644 --- a/tests/phpunit/includes/PropertyDisparityDetectorTest.php +++ b/tests/phpunit/includes/PropertyDisparityDetectorTest.php @@ -86,7 +86,6 @@ $this->assertInstanceOf( $this->getClass(), $this->getInstance() ); } - /** * @test PropertyDisparityDetector::detectDisparity * @dataProvider dataItemDataProvider @@ -95,7 +94,7 @@ */ public function testFindDisparity( $storeValues, $dataValues, $settings, $expected ) { - $title = $this->getTitle( SMW_NS_PROPERTY ); + $subject = $this->newSubject( $this->newTitle( SMW_NS_PROPERTY ) ); $this->storeValues = $storeValues; $store = array( @@ -103,16 +102,19 @@ ); $data = array( - 'getSubject' => $this->newSubject( $title ), + 'getSubject' => $subject, 'getPropertyValues' => $dataValues ); $instance = $this->getInstance( $store, $data, $settings ); + $observer = new MockChangeObserver( $instance ); $this->assertInstanceOf( $this->getClass(), $instance->detectDisparity() ); - $this->assertEquals( $expected['disp'], $instance->hasDisparity() ); - $this->assertInternalType( $expected['type'], $instance->getDispatcherJob() ); - $this->assertEquals( $expected['count'], count( $instance->getDispatcherJob() ) ); + $this->assertEquals( $subject->getTitle(), $instance->getTitle() ); + $this->assertEquals( $expected['change'], $instance->hasDisparity() ); + + // Verify that the Observer was notified + $this->assertEquals( $expected['notifier'], $observer->getNotifier() ); } @@ -123,10 +125,14 @@ */ public function dataItemDataProvider() { + $notifier = 'runUpdateDispatcher'; + + // Single $subject = array( $this->newSubject() ); + // Multiple $subjects = array( $this->newSubject(), $this->newSubject(), @@ -135,15 +141,15 @@ return array( // $storeValues, $dataValues, $settings, $expected - array( $subjects, array(), array( '_PVAL', '_LIST' ), array( 'disp' => true, 'type' => 'array', 'count' => 1 ) ), - array( array(), $subjects, array( '_PVAL', '_LIST' ), array( 'disp' => true, 'type' => 'array', 'count' => 1 ) ), - array( $subject, $subjects, array( '_PVAL', '_LIST' ), array( 'disp' => true, 'type' => 'array', 'count' => 1 ) ), - array( $subject, array(), array( '_PVAL', '_LIST' ), array( 'disp' => true, 'type' => 'array', 'count' => 1 ) ), - array( $subject, array(), array( '_PVAL' ), array( 'disp' => true, 'type' => 'array', 'count' => 1 ) ), - array( $subjects, $subjects, array( '_PVAL' ), array( 'disp' => false, 'type' => 'null', 'count' => 0 ) ), - array( $subject, $subject, array( '_PVAL' ), array( 'disp' => false, 'type' => 'null', 'count' => 0 ) ), - array( $subjects, $subjects, array( '_PVAL', '_LIST' ), array( 'disp' => true, 'type' => 'array', 'count' => 1 ) ), - array( $subject, $subject, array( '_PVAL', '_LIST' ), array( 'disp' => true, 'type' => 'array', 'count' => 1 ) ) + array( $subjects, array(), array( '_PVAL', '_LIST' ), array( 'change' => true, 'notifier' => $notifier ) ), + array( array(), $subjects, array( '_PVAL', '_LIST' ), array( 'change' => true, 'notifier' => $notifier ) ), + array( $subject, $subjects, array( '_PVAL', '_LIST' ), array( 'change' => true, 'notifier' => $notifier ) ), + array( $subject, array(), array( '_PVAL', '_LIST' ), array( 'change' => true, 'notifier' => $notifier ) ), + array( $subject, array(), array( '_PVAL' ), array( 'change' => true, 'notifier' => $notifier ) ), + array( $subjects, $subjects, array( '_PVAL' ), array( 'change' => false, 'notifier' => null ) ), + array( $subject, $subject, array( '_PVAL' ), array( 'change' => false, 'notifier' => null ) ), + array( $subjects, $subjects, array( '_PVAL', '_LIST' ), array( 'change' => true, 'notifier' => $notifier ) ), + array( $subject, $subject, array( '_PVAL', '_LIST' ), array( 'change' => true, 'notifier' => $notifier ) ) ); } diff --git a/tests/phpunit/includes/utilities/ChangeObserverTest.php b/tests/phpunit/includes/utilities/ChangeObserverTest.php new file mode 100644 index 0000000..48df813 --- /dev/null +++ b/tests/phpunit/includes/utilities/ChangeObserverTest.php @@ -0,0 +1,179 @@ +<?php + +namespace SMW\Test; + +use SMW\ChangeObserver; + +/** + * Tests for the ChangeObserver class + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ + +/** + * @covers \SMW\ChangeObserver + * + * @ingroup Test + * + * @group SMW + * @group SMWExtension + */ +class ChangeObserverTest extends SemanticMediaWikiTestCase { + + /** + * Returns the name of the class to be tested + * + * @return string|false + */ + public function getClass() { + return '\SMW\ChangeObserver'; + } + + /** + * Helper method that returns a ChangeObserver object + * + * @since 1.9 + * + * @param $data + * + * @return ChangeObserver + */ + private function getInstance() { + return new ChangeObserver(); + } + + /** + * @test ChangeObserver::__construct + * + * @since 1.9 + */ + public function testConstructor() { + $this->assertInstanceOf( $this->getClass(), $this->getInstance() ); + } + + /** + * @test ChangeObserver::getStore + * @test ChangeObserver::setStore + * + * @since 1.9 + */ + public function testGetSetStore() { + + $instance = $this->getInstance(); + $mockStore = $this->newMockObject()->getMockStore(); + + $this->assertInstanceOf( '\SMW\Store', $instance->getStore() ); + $instance->setStore( $mockStore ); + $this->assertInstanceOf( '\SMW\Store', $instance->getStore() ); + $this->assertEquals( $mockStore, $instance->getStore() ); + + } + + /** + * @test ChangeObserver::getCache + * + * @since 1.9 + */ + public function testGetCache() { + + $instance = $this->getInstance(); + $this->assertInstanceOf( '\SMW\CacheHandler', $instance->getCache() ); + } + + /** + * @test ChangeObserver::setSettings + * @test ChangeObserver::getSettings + * @dataProvider titleDataProvider + * + * @since 1.9 + */ + public function testGetSetSettings( $setup ) { + + $instance = $this->getInstance(); + $settings = $this->getSettings( $setup['settings'] ); + + $this->assertInstanceOf( '\SMW\Settings', $instance->getSettings() ); + + $instance->setSettings( $settings ) ; + $this->assertEquals( $settings , $instance->getSettings() ); + + } + + /** + * @test ChangeObserver::runUpdateDispatcher + * @dataProvider titleDataProvider + * + * @since 1.9 + */ + public function testUpdateDispatcherJob( $setup, $expected ) { + + $instance = $this->getInstance(); + $instance->setSettings( $this->getSettings( $setup['settings'] ) ); + + $this->assertTrue( $instance->runUpdateDispatcher( $setup['title'] ) ); + } + + /** + * @note smwgEnableUpdateJobs is set false to avoid having a Job being + * pushed into the "real" JobQueue + * + * @return array + */ + public function titleDataProvider() { + + $title = $this->getMockForAbstractClass( '\SMW\TitleProvider' ); + + $title->expects( $this->any() ) + ->method( 'getTitle' ) + ->will( $this->returnValue( $this->newTitle() ) ); + + $provider = array(); + + // #0 + $provider[] = array( + array( + 'settings' => array( + 'smwgEnableUpdateJobs' => false, + 'smwgDeferredPropertyUpdate' => false + ), + 'title' => $title + ), + array() + ); + + // #1 + $provider[] = array( + array( + 'settings' => array( + 'smwgEnableUpdateJobs' => false, + 'smwgDeferredPropertyUpdate' => true + ), + 'title' => $title + ), + array() + ); + + return $provider; + } + +} -- To view, visit https://gerrit.wikimedia.org/r/75850 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ifd8604d8ed551a62e3aa5c160e536c5cb1d0e718 Gerrit-PatchSet: 7 Gerrit-Project: mediawiki/extensions/SemanticMediaWiki Gerrit-Branch: master Gerrit-Owner: Mwjames <[email protected]> Gerrit-Reviewer: Jeroen De Dauw <[email protected]> Gerrit-Reviewer: Mwjames <[email protected]> Gerrit-Reviewer: jenkins-bot _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
