jenkins-bot has submitted this change and it was merged.

Change subject: \SMW\ApiBrowse enables to browse a subject via 
api.php?action=browse
......................................................................


\SMW\ApiBrowse enables to browse a subject via api.php?action=browse

Code coverage: 100%
CRAP: 22

This is the corresponding Api for the Special:Browse interface (without
support of incoming properties).

In order to work with the Api, the following schema [2] can be used.

- Uses the new SMW\Serializer [1]
- JSON and XML output are supported

[1] https://gerrit.wikimedia.org/r/#/c/89098/

[2] api.php?action=browse&subject=Main_Page

Change-Id: I00c2fab386d0c74ef0b576876edb2f873c17bfde
---
M SemanticMediaWiki.classes.php
A docs/api.md
M includes/Setup.php
A includes/api/ApiBrowse.php
M includes/dataitems/SMW_DI_Property.php
M tests/phpunit/MockObjectRepository.php
M tests/phpunit/includes/SetupTest.php
A tests/phpunit/includes/api/ApiBrowseSerializationRoundtripTest.php
A tests/phpunit/includes/api/ApiBrowseTest.php
9 files changed, 663 insertions(+), 2 deletions(-)

Approvals:
  Mwjames: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/SemanticMediaWiki.classes.php b/SemanticMediaWiki.classes.php
index 7f6544a..db6248f 100644
--- a/SemanticMediaWiki.classes.php
+++ b/SemanticMediaWiki.classes.php
@@ -359,6 +359,7 @@
        'SMW\ApiAsk'     => 'includes/api/ApiAsk.php',
        'SMW\ApiAskArgs' => 'includes/api/ApiAskArgs.php',
        'SMW\ApiInfo'    => 'includes/api/ApiInfo.php',
+       'SMW\ApiBrowse'  => 'includes/api/ApiBrowse.php',
 
        // Maintenance scripts
        'SMWSetupScript' => 'maintenance/SMW_setup.php',
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..2ca1e54
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,128 @@
+This file contains details about Semantic MediaWiki's API for external use 
with a description that is reflecting the current master branch (SMW 1.9). For 
more details on "how to use" MediaWiki's WebAPI, it is recommended to read this 
[website][api].
+
+## AskApi
+The Ask API allows you to do ask queries against SMW using the MediaWiki API 
and get results back serialized in one of the formats it supports.
+
+The ask module supports one parameter, query, which takes the same string 
you'd feed into an #ask tag, but urlencoded.
+
+> api.php?action=ask&query=[[Modification date::%2B]]|%3FModification 
date|sort%3DModification date|order%3Ddesc&format=jsonfm
+
+### AskArgsApi
+The Askargs module aims to take arguments in un-serialized form, so with as 
little ask-specific syntax as possible. It supports 3 arguments:
+
+* "conditions": The query conditions, ie the requirements for a subject to be 
included
+* "printouts": The query printeouts, ie the properties to show per subject
+* "parameters": The query parameters, ie all non-condition and non-printeout 
arguments
+
+> api.php?action=askargs&conditions=Modification 
date::%2B&printouts=Modification date&parameters=|sort%3DModification 
date|order%3Ddesc&format=jsonfm
+
+#### Output serialization
+```php
+{
+       "query-continue-offset": 50,
+       "query": {
+               "printrequests": [
+                       {
+                               "label": "",
+                               "typeid": "_wpg",
+                               "mode": 2,
+                               "format": false
+                       },
+                       {
+                               "label": "Modification date",
+                               "typeid": "_dat",
+                               "mode": 1,
+                               "format": ""
+                       }
+               ],
+               "results": {
+                       "Main Page": {
+                               "printouts": {
+                                       "Modification date": [
+                                               "1381456128"
+                                       ]
+                               },
+                               "fulltext": "Main Page",
+                               "fullurl": 
"http:\/\/localhost:8080\/mw\/index.php\/Main_Page",
+                               "namespace": 0,
+                               "exists": true
+                       },
+                       ...
+               },
+               "meta": {
+                       "hash": "a9abdb34024fa8735f6b044305a48619",
+                       "count": 50,
+                       "offset": 0
+               }
+       }
+}
+```
+
+## AskInfo
+An interface to access statistical information about the properties, values 
etc..
+
+> api.php?action=smwinfo&format=json&info=proppagecount|propcount
+
+The following parameters are available and can be concatenate using the "|" 
character.
+* proppagecount
+* propcount
+* querycount
+* usedpropcount
+* declaredpropcount
+* conceptcount
+* querysize
+* subobjectcount
+* formatcount
+
+#### Output serialization
+
+```php
+{
+       "info": {
+               "proppagecount": 40,
+               "formatcount": {
+                       "table": 14,
+                       "list": 3,
+                       "broadtable": 1
+               }
+       }
+}
+```
+The parameter "formatcount" will output an array of used formats together with 
its count information.
+
+## BrowseApi
+An interface to browse facts of a subject (wikipage).
+
+> api.php?action=browse&subject=Main%20Page
+
+#### Output serialization
+
+```php
+{
+       "query": {
+               "subject": "Main_Page#0#",
+               "data": [
+                       {
+                               "property": "Foo",
+                               "dataitem": [
+                                       {
+                                               "type": 2,
+                                               "item": "Bar"
+                                       }
+                               ]
+                       },
+               ...
+               "serializer": "SMW\Serializers\SemanticDataSerializer",
+               "version": 0.1
+       }
+}
+```
+The output is generated using the <code>SMW\SerializerFactory</code> which if 
necessary can also be used to un-serialize the data received from the Api. For 
details about the output format and how to use 
<code>SMW\SerializerFactory</code>, see <code>/docs/serializer.md</code>.
+
+```php
+$pai = new ApiBrowse( ... )
+$result = $api->getResultData();
+$semanticData = SerializerFactory::deserialize( $result['query'] );
+```
+
+[api]: https://www.mediawiki.org/wiki/Api "Manual:Api"
\ No newline at end of file
diff --git a/includes/Setup.php b/includes/Setup.php
index 3a59cbd..fed4ac9 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -151,6 +151,7 @@
                $this->globals['wgAPIModules']['smwinfo'] = '\SMW\ApiInfo';
                $this->globals['wgAPIModules']['ask']     = '\SMW\ApiAsk';
                $this->globals['wgAPIModules']['askargs'] = '\SMW\ApiAskArgs';
+               $this->globals['wgAPIModules']['browse']  = '\SMW\ApiBrowse';
 
        }
 
diff --git a/includes/api/ApiBrowse.php b/includes/api/ApiBrowse.php
new file mode 100644
index 0000000..b7525a2
--- /dev/null
+++ b/includes/api/ApiBrowse.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace SMW;
+
+use Title;
+
+/**
+ * Api module to browse a subject
+ *
+ * @since 1.9
+ *
+ * @licence GNU GPL v2+
+ * @author mwjames
+ */
+class ApiBrowse extends ApiBase {
+
+       /**
+        * @see ApiBase::execute
+        */
+       public function execute() {
+
+               $params = $this->extractRequestParams();
+
+               $serialized = SerializerFactory::serialize(
+                       $this->getSemanticData( $this->getSubject( 
$params['subject'] ) )
+               );
+
+               $this->getResult()->addValue( null, 'query', 
$this->runFormatter( $serialized ) );
+       }
+
+       /**
+        * @since 1.9
+        */
+       protected function getSubject( $text ) {
+               return DIWikiPage::newFromTitle( $this->newFromText( $text ) );
+       }
+
+       /**
+        * @codeCoverageIgnore
+        * @since 1.9
+        */
+       protected function newFromText( $text ) {
+
+               $title = Title::newFromText( $text );
+
+               if ( $title instanceOf Title ) {
+                       return $title;
+               }
+
+               $this->dieUsageMsg( array( 'invalidtitle', $text ) );
+       }
+
+       /**
+        * @since 1.9
+        */
+       protected function getSemanticData( DIWikiPage $subject ) {
+
+               $semanticData = $this->getStore()->getSemanticData( $subject );
+
+               foreach ( $semanticData->getProperties() as $property ) {
+                       if ( $property->getKey() === DIProperty::TYPE_SUBOBJECT 
|| $property->getKey() === DIProperty::TYPE_ASKQUERY ) {
+                               $this->addSubSemanticData( $property, 
$semanticData );
+                       }
+               }
+
+               return $semanticData;
+       }
+
+       /**
+        * @note In case where the original SemanticData container does not 
include
+        * subobjects, this method will add them to ensure a "complete object" 
for
+        * all available entities that belong to this subject (excluding 
incoming
+        * properties)
+        *
+        * @note If the subobject already exists within the current SemanticData
+        * instance it will not be imported again (this avoids calling the Store
+        * again)
+        *
+        * @since 1.9
+        */
+       protected function addSubSemanticData( $property, &$semanticData ) {
+
+               $subSemanticData = $semanticData->getSubSemanticData();
+
+               foreach ( $semanticData->getPropertyValues( $property ) as 
$value ) {
+                       if ( $value instanceOf DIWikiPage && !isset( 
$subSemanticData[ $value->getSubobjectName() ] ) ) {
+                               $semanticData->addSubSemanticData( 
$this->getStore()->getSemanticData( $value ) );
+                       }
+               }
+       }
+
+       /**
+        * @since 1.9
+        */
+       protected function runFormatter( $serialized ) {
+
+               $this->addIndexTags( $serialized );
+
+               if ( isset( $serialized['sobj'] ) ) {
+
+                       $this->getResult()->setIndexedTagName( 
$serialized['sobj'], 'subobject' );
+
+                       foreach ( $serialized['sobj'] as $key => &$value ) {
+                               $this->addIndexTags( $value );
+                       }
+               }
+
+               return $serialized;
+       }
+
+       /**
+        * @since 1.9
+        */
+       protected function addIndexTags( &$serialized ) {
+
+               if ( isset( $serialized['data'] ) ) {
+
+                       $this->getResult()->setIndexedTagName( 
$serialized['data'], 'property' );
+
+                       foreach ( $serialized['data'] as $key => $value ) {
+                               if ( isset(  $serialized['data'][ $key 
]['dataitem'] ) ) {
+                                       $this->getResult()->setIndexedTagName( 
$serialized['data'][ $key ]['dataitem'], 'value' );
+                               }
+                       }
+               }
+
+       }
+
+       /**
+        * @codeCoverageIgnore
+        * @see ApiBase::getAllowedParams
+        *
+        * @return array
+        */
+       public function getAllowedParams() {
+               return array(
+                       'subject' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_ISMULTI => false,
+                               ApiBase::PARAM_REQUIRED => true,
+                       )
+               );
+       }
+
+       /**
+        * @codeCoverageIgnore
+        * @see ApiBase::getParamDescription
+        *
+        * @return array
+        */
+       public function getParamDescription() {
+               return array(
+                       'subject' => 'The subject to be queried',
+               );
+       }
+
+       /**
+        * @codeCoverageIgnore
+        * @see ApiBase::getDescription
+        *
+        * @return array
+        */
+       public function getDescription() {
+               return array(
+                       'API module to query a subject.'
+               );
+       }
+
+       /**
+        * @codeCoverageIgnore
+        * @see ApiBase::getExamples
+        *
+        * @return array
+        */
+       protected function getExamples() {
+               return array(
+                       'api.php?action=browse&subject=Main_Page',
+               );
+       }
+
+       /**
+        * @codeCoverageIgnore
+        * @see ApiBase::getVersion
+        *
+        * @return string
+        */
+       public function getVersion() {
+               return __CLASS__ . '-' . SMW_VERSION;
+       }
+
+}
diff --git a/includes/dataitems/SMW_DI_Property.php 
b/includes/dataitems/SMW_DI_Property.php
index 5642cc3..769f3eb 100644
--- a/includes/dataitems/SMW_DI_Property.php
+++ b/includes/dataitems/SMW_DI_Property.php
@@ -43,6 +43,8 @@
        const TYPE_HAS_TYPE = '_TYPE';
        // Property "corresponds to"
        const TYPE_CONVERSION = '_CONV';
+       // Property "has query"
+       const TYPE_ASKQUERY = '_ASK';
 
        /**
         * Array for assigning types to predefined properties. Each
@@ -408,7 +410,7 @@
                                '_SF_DF' => array( '__spf', true ), // Semantic 
Form's default form property
                                '_SF_AF' => array( '__spf', true ),  // 
Semantic Form's alternate form property
                                self::TYPE_SUBOBJECT =>  array( '_wpg', true ), 
// "has subobject"
-                               '_ASK'   =>  array( '_wpg', false ), // "has 
query"
+                               self::TYPE_ASKQUERY  =>  array( '_wpg', false 
), // "has query"
                                '_ASKST' =>  array( '_cod', true ), // "has 
query string"
                                '_ASKFO' =>  array( '_txt', true ), // "has 
query format"
                                '_ASKSI' =>  array( '_num', true ), // "has 
query size"
diff --git a/tests/phpunit/MockObjectRepository.php 
b/tests/phpunit/MockObjectRepository.php
index c768114..5cc885f 100644
--- a/tests/phpunit/MockObjectRepository.php
+++ b/tests/phpunit/MockObjectRepository.php
@@ -642,7 +642,7 @@
 
                $store->expects( $this->any() )
                        ->method( 'getSemanticData' )
-                       ->will( $this->returnValue( $this->builder->setValue( 
'getSemanticData' ) ) );
+                       ->will( $this->builder->setCallback( 'getSemanticData' 
) );
 
                $store->expects( $this->any() )
                        ->method( 'getUnusedPropertiesSpecial' )
diff --git a/tests/phpunit/includes/SetupTest.php 
b/tests/phpunit/includes/SetupTest.php
index 9b134cf..e8b28cb 100644
--- a/tests/phpunit/includes/SetupTest.php
+++ b/tests/phpunit/includes/SetupTest.php
@@ -394,6 +394,7 @@
                        'ask',
                        'smwinfo',
                        'askargs',
+                       'browse',
                );
 
                foreach ( $modules as $module ) {
diff --git a/tests/phpunit/includes/api/ApiBrowseSerializationRoundtripTest.php 
b/tests/phpunit/includes/api/ApiBrowseSerializationRoundtripTest.php
new file mode 100644
index 0000000..55c6f06
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiBrowseSerializationRoundtripTest.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace SMW\Test;
+
+use SMW\SerializerFactory;
+use SMW\DataValueFactory;
+use SMW\ApiBrowse;
+use SMW\SemanticData;
+use SMW\DIWikiPage;
+use SMW\Subobject;
+
+/**
+ * @covers \SMW\ApiBrowse
+ *
+ * @since 1.9
+ *
+ * @group SMW
+ * @group SMWExtension
+ * @group API
+ *
+ * @licence GNU GPL v2+
+ * @author mwjames
+ */
+class ApiBrowseSerializationRoundtripTest extends ApiTestCase {
+
+       /**
+        * @return string|false
+        */
+       public function getClass() {
+               return '\SMW\ApiBrowse';
+       }
+
+       /**
+        * @since 1.9
+        */
+       private function newSemanticData( $text ) {
+               return $data = new SemanticData( DIWikiPage::newFromTitle( 
$this->newTitle( NS_MAIN, $text ) ) );
+       }
+
+       /**
+        * @dataProvider semanticDataProvider
+        *
+        * @since 1.9
+        */
+       public function testExecuteOnMockStore( $setup ) {
+
+               $api = new ApiBrowse( $this->getApiMain( array( 'subject' => 
$setup['subject'] ) ), 'browse' );
+               $api->setStore( $setup['store'] );
+               $api->execute();
+
+               $result = $api->getResultData();
+
+               $this->assertStructuralIntegrity( $setup, $result );
+
+               // We gimmick a bit here otherwise matching the array will be 
cumbersome,
+               // we'll use the result from the Api and recreate (using the 
Serializer)
+               // the SemanticData container and then compare the hash from 
the original
+               // container with that of the newly created container from the 
Api
+               $this->assertEquals(
+                       $setup['data']->getHash(),
+                       SerializerFactory::deserialize( $result['query'] 
)->getHash(),
+                       'Asserts that getHash() compares to both SemanticData 
containers'
+               );
+
+       }
+
+       /**
+        * The Serializer enforces a specific output format therefore expected
+        * elements are verified
+        *
+        * @since 1.9
+        */
+       public function assertStructuralIntegrity( $type, $result ) {
+
+               if ( isset( $type['hasError'] ) && $type['hasError'] ) {
+                       $this->assertInternalType( 'array', $result['error'] );
+               }
+
+               if ( isset( $type['hasResult'] ) && $type['hasResult'] ) {
+                       $this->assertInternalType( 'array', $result['query'] );
+               }
+
+               if ( isset( $type['hasSubject'] ) && $type['hasSubject'] ) {
+                       $this->assertInternalType( 'string', 
$result['query']['subject'] );
+               }
+
+               if ( isset( $type['hasData'] ) && $type['hasData'] ) {
+                       $this->assertInternalType( 'array', 
$result['query']['data'] );
+               }
+
+               if ( isset( $type['hasSobj'] ) && $type['hasSobj'] ) {
+                       $this->assertInternalType( 'array', 
$result['query']['sobj'] );
+               }
+
+       }
+
+       /**
+        * @return array
+        */
+       public function semanticDataProvider() {
+
+               $provider = array();
+
+               // #0 Empty container
+               $data = $this->newSemanticData( 'Foo-0' );
+
+               $mockStore = $this->newMockBuilder()->newObject( 'Store', array(
+                       'getSemanticData' => $data
+               ) );
+
+               $provider[] = array(
+                       array(
+                               'subject' => 'Foo-0',
+                               'store'   => $mockStore,
+                               'data'    => $data,
+                               'hasData' => false,
+                               'hasSobj' => false
+                       )
+               );
+
+               // #1 Single entry
+               $data = $this->newSemanticData( 'Foo-1' );
+               $data->addDataValue( DataValueFactory::newPropertyValue( 'Has 
fooQuex', 'Bar' ) );
+
+               $mockStore = $this->newMockBuilder()->newObject( 'Store', array(
+                       'getSemanticData' => $data
+               ) );
+
+               $provider[] = array(
+                       array(
+                               'subject' => 'Foo-1',
+                               'store'   => $mockStore,
+                               'data'    => $data,
+                               'hasData' => true,
+                               'hasSobj' => false
+                       )
+               );
+
+               // #2 Single + single subobject entry
+               $title = $this->newTitle( NS_MAIN, 'Foo-2' );
+               $data  = $this->newSemanticData( 'Foo-2' );
+               $data->addDataValue( DataValueFactory::newPropertyValue( 'Has 
fooQuex', 'Bar' ) );
+
+               $subobject = new Subobject( $title );
+               $subobject->setSemanticData( 'Foo-sub' );
+               $subobject->addDataValue( DataValueFactory::newPropertyValue( 
'Has subobjects', 'Bam' ) );
+
+               // Adding a reference but not the container itself
+               $data->addPropertyObjectValue( $subobject->getProperty(), 
$subobject->getSemanticData()->getSubject() );
+
+               $mockStore = $this->newMockBuilder()->newObject( 'Store', array(
+                       'getSemanticData' => function ( $subject ) use( $data, 
$subobject ) {
+                               return $subject->getSubobjectName() === 
'Foo-sub' ? $subobject->getSemanticData() : $data;
+                       }
+               ) );
+
+               $provider[] = array(
+                       array(
+                               'subject' => 'Foo-2',
+                               'store'   => $mockStore,
+                               'data'    => $data,
+                               'hasData' => true,
+                               'hasSobj' => true
+                       )
+               );
+
+               // #3 Single + single subobject where the subobject already 
exists
+               $title = $this->newTitle( NS_MAIN, 'Foo-3' );
+               $data  = $this->newSemanticData( 'Foo-3' );
+               $data->addDataValue( DataValueFactory::newPropertyValue( 'Has 
fooQuex', 'Bar' ) );
+
+               $subobject = new Subobject( $title );
+               $subobject->setSemanticData( 'Foo-sub' );
+               $subobject->addDataValue( DataValueFactory::newPropertyValue( 
'Has subobjects', 'Bam' ) );
+
+               $data->addPropertyObjectValue( $subobject->getProperty(), 
$subobject->getContainer() );
+
+               $mockStore = $this->newMockBuilder()->newObject( 'Store', array(
+                       'getSemanticData' => $data
+               ) );
+
+               $provider[] = array(
+                       array(
+                               'subject' => 'Foo-3',
+                               'store'   => $mockStore,
+                               'data'    => $data,
+                               'hasData' => true,
+                               'hasSobj' => true
+                       )
+               );
+
+               return $provider;
+       }
+
+}
diff --git a/tests/phpunit/includes/api/ApiBrowseTest.php 
b/tests/phpunit/includes/api/ApiBrowseTest.php
new file mode 100644
index 0000000..3e14073
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiBrowseTest.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace SMW\Test;
+
+use SMW\ApiBrowse;
+use SMW\SemanticData;
+use SMW\DIWikiPage;
+
+/**
+ * @covers \SMW\ApiBrowse
+ *
+ * @since 1.9
+ *
+ * @group SMW
+ * @group SMWExtension
+ * @group API
+ *
+ * @licence GNU GPL v2+
+ * @author mwjames
+ */
+class ApiBrowseTest extends ApiTestCase {
+
+       /**
+        * @return string|false
+        */
+       public function getClass() {
+               return '\SMW\ApiBrowse';
+       }
+
+       /**
+        * @since 1.9
+        */
+       private function newSemanticData( $text ) {
+               return $data = new SemanticData( DIWikiPage::newFromTitle( 
$this->newTitle( NS_MAIN, $text ) ) );
+       }
+
+       /**
+        * @dataProvider subjectDataProvider
+        *
+        * @since 1.9
+        */
+       public function testExecuteOnSQLStore( $setup ) {
+
+               $this->runOnlyOnSQLStore();
+
+               $result = $this->doApiRequest( array(
+                       'action'  => 'browse',
+                       'subject' => $setup['subject']
+               ) );
+
+               $this->assertStructuralIntegrity( $setup, $result );
+
+       }
+
+       /**
+        * @dataProvider invalidTitleDataProvider
+        *
+        * @since 1.9
+        */
+       public function testInvalidTitleExecuteOnSQLStore( $setup ) {
+
+               $this->runOnlyOnSQLStore();
+
+               $this->setExpectedException( 'Exception' );
+
+               $result = $this->doApiRequest( array(
+                       'action'  => 'browse',
+                       'subject' => $setup['subject']
+               ) );
+
+               $this->assertStructuralIntegrity( $setup, $result );
+
+       }
+
+       /**
+        * The Serializer enforces a specific output format therefore expected
+        * elements are verified
+        *
+        * @since 1.9
+        */
+       public function assertStructuralIntegrity( $type, $result ) {
+
+               if ( isset( $type['hasError'] ) && $type['hasError'] ) {
+                       $this->assertInternalType( 'array', $result['error'] );
+               }
+
+               if ( isset( $type['hasResult'] ) && $type['hasResult'] ) {
+                       $this->assertInternalType( 'array', $result['query'] );
+               }
+
+               if ( isset( $type['hasSubject'] ) && $type['hasSubject'] ) {
+                       $this->assertInternalType( 'string', 
$result['query']['subject'] );
+               }
+
+               if ( isset( $type['hasData'] ) && $type['hasData'] ) {
+                       $this->assertInternalType( 'array', 
$result['query']['data'] );
+               }
+
+               if ( isset( $type['hasSobj'] ) && $type['hasSobj'] ) {
+                       $this->assertInternalType( 'array', 
$result['query']['sobj'] );
+               }
+
+       }
+
+       /**
+        * @return array
+        */
+       public function subjectDataProvider() {
+
+               $provider = array();
+
+               // #0 Valid
+               $provider[] = array(
+                       array(
+                               'subject'    => 'Main_Page',
+                               'hasSubject' => true,
+                               'hasResult'  => true
+                       )
+               );
+
+               return $provider;
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidTitleDataProvider() {
+
+               $provider = array();
+
+               $provider[] = array(
+                       array(
+                               'subject'   => '{}',
+                               'hasError'  => true,
+                               'hasResult' => false
+                       )
+               );
+
+               return $provider;
+       }
+
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/89147
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I00c2fab386d0c74ef0b576876edb2f873c17bfde
Gerrit-PatchSet: 9
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

Reply via email to