Foxtrott has uploaded a new change for review.
https://gerrit.wikimedia.org/r/278682
Change subject: Unit test BasicBackend
......................................................................
Unit test BasicBackend
Change-Id: I5ffeb4d4667a14349616b13c400540a9eac5d7c7
---
M i18n/en.json
M i18n/qqq.json
M src/BasicBackend.php
M tests/phpunit/Unit/BasicBackendTest.php
4 files changed, 444 insertions(+), 102 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Lingo
refs/changes/82/278682/1
diff --git a/i18n/en.json b/i18n/en.json
index 2585912..b4904f7 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -4,6 +4,8 @@
},
"lingo-desc": "Provides hover-over tool tips on pages from words
defined on the [[$1]] page",
"lingo-terminologypagename": "Terminology",
- "lingo-noterminologypage": "Page \"$1\" does not exist.",
- "lingo-terminologypagenotlocal": "Page \"$1\" is not a local page."
-}
\ No newline at end of file
+ "lingo-noterminologypage": "The terminology page \"$1\" does not
exist.",
+ "lingo-notatextpage": "The terminology page \"$1\" is not a text page.",
+ "lingo-terminologypagenotlocal": "Page \"$1\" is not a local page.",
+ "lingo-noapprovedrevs": "Support for ApprovedRevs is enabled in Lingo.
But ApprovedRevs was not found."
+}
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 1b05962..3924b3a 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -7,6 +7,8 @@
},
"lingo-desc":
"{{desc|name=Lingo|url=https://www.mediawiki.org/wiki/Extension:Lingo}}\nParameters:\n*
$1 - terminology page title (value of <code>$wgexLingoPage</code>), or
{{msg-mw|Lingo-terminologypagename}}",
"lingo-terminologypagename": "Name of the page where the terms and
definitions of the glossary are stored",
- "lingo-noterminologypage": "Used as warning. Parameters:\n* $1 -
terminology page title (value of <code>$wgexLingoPage</code>), or
{{msg-mw|lingo-terminologypagename}}\nSee also:\n*
{{msg-mw|lingo-terminologypagenotlocal}}",
- "lingo-terminologypagenotlocal": "Used as error message. Parameters:\n*
$1 - terminology page title (value of <code>$wgexLingoPage</code>), or
{{msg-mw|lingo-terminologypagename}}\nSee also:\n*
{{msg-mw|lingo-noterminologypage}}"
+ "lingo-noterminologypage": "Used as warning message. Parameters:\n* $1
- terminology page title (value of <code>$wgexLingoPage</code>), or
{{msg-mw|lingo-terminologypagename}}\nSee also:\n*
{{msg-mw|lingo-terminologypagenotlocal}}",
+ "lingo-notatextpage": "Used as error message. Parameters:\n* $1 -
terminology page title (value of <code>$wgexLingoPage</code>), or
{{msg-mw|lingo-terminologypagename}}\nSee also:\n*
{{msg-mw|lingo-noterminologypage}}",
+ "lingo-terminologypagenotlocal": "Used as error message. Parameters:\n*
$1 - terminology page title (value of <code>$wgexLingoPage</code>), or
{{msg-mw|lingo-terminologypagename}}\nSee also:\n*
{{msg-mw|lingo-noterminologypage}}",
+ "lingo-noapprovedrevs": "Used as warning message when the ApprovedRevs
extension is not installed."
}
diff --git a/src/BasicBackend.php b/src/BasicBackend.php
index 114a6da..2675c39 100644
--- a/src/BasicBackend.php
+++ b/src/BasicBackend.php
@@ -33,6 +33,7 @@
use Parser;
use ParserOptions;
use Revision;
+use TextContent;
use Title;
use User;
use WikiPage;
@@ -64,14 +65,6 @@
}
/**
- * @return string
- */
- private function getLingoPage() {
- global $wgexLingoPage;
- return $wgexLingoPage ? $wgexLingoPage : wfMessage(
'lingo-terminologypagename' )->inContentLanguage()->text();
- }
-
- /**
* This function returns the next element. The element is an array of
four
* strings: Term, Definition, Link, Source. For the Lingo\BasicBackend
Link
* and Source are set to null. If there is no next element the function
@@ -85,33 +78,18 @@
static $definitions = array();
static $ret = array();
- $this->setArticleLines();
+ $this->collectDictionaryLines();
- // find next valid line (yes, the assignation is intended)
- while ( ( count( $ret ) == 0 ) && ( $entry = each(
$this->mArticleLines ) ) ) {
+ // loop backwards: accumulate definitions until term found
+ while ( ( count( $ret ) === 0 ) && ( $this->mArticleLines ) ) {
- if ( empty( $entry[ 1 ] ) || ( $entry[ 1 ][ 0 ] !== ';'
&& $entry[ 1 ][ 0 ] !== ':' ) ) {
+ $line = array_pop( $this->mArticleLines );
+
+ if ( empty( $line ) || ( $line[ 0 ] !== ';' && $line[ 0
] !== ':' ) ) {
continue;
}
- $chunks = explode( ':', $entry[ 1 ], 2 );
-
- // found a new definition?
- if ( count( $chunks ) == 2 ) {
-
- // wipe the data if its a totaly new term
definition
- if ( !empty( $term ) && count( $definitions ) >
0 ) {
- $definitions = array();
- $term = null;
- }
-
- $definitions[] = trim( $chunks[ 1 ] );
- }
-
- // found a new term?
- if ( count( $chunks ) >= 1 && strlen( $chunks[ 0 ] ) >=
1 ) {
- $term = trim( substr( $chunks[ 0 ], 1 ) );
- }
+ $this->queueNextLine( $line, $term, $definitions );
if ( $term !== null ) {
foreach ( $definitions as $definition ) {
@@ -129,25 +107,159 @@
}
/**
+ * @throws \MWException
+ */
+ protected function collectDictionaryLines() {
+
+ if ( $this->mArticleLines !== null ) {
+ return;
+ }
+
+ // Get Terminology page
+ $dictionaryPageName = $this->getLingoPageName();
+ $dictionaryTitle = $this->getTitleFromText( $dictionaryPageName
);
+
+ if ( $dictionaryTitle->getInterwiki() !== '' ) {
+ $this->getMessageLog()->addError( wfMessage(
'lingo-terminologypagenotlocal', $dictionaryPageName
)->inContentLanguage()->text() );
+ return;
+ }
+
+ $rawContent = $this->getRawDictionaryContent( $dictionaryTitle
);
+
+ // Expand templates and variables in the text, producing valid,
static
+ // wikitext. Have to use a new anonymous user to avoid any
leakage as
+ // Lingo is caching only one user-independent glossary
+ $parser = new Parser;
+ $content = $parser->preprocess( $rawContent, $dictionaryTitle,
new ParserOptions( new User() ) );
+
+ $this->mArticleLines = explode( "\n", $content );
+ }
+
+ /**
+ * @return string
+ */
+ private function getLingoPageName() {
+ global $wgexLingoPage;
+ return $wgexLingoPage ? $wgexLingoPage : wfMessage(
'lingo-terminologypagename' )->inContentLanguage()->text();
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @param $dictionaryPage
+ * @return null|Title
+ */
+ protected function getTitleFromText( $dictionaryPage ) {
+ return Title::newFromText( $dictionaryPage );
+ }
+
+ /**
+ * @param Title $dictionaryTitle
+ * @return null|string
+ * @throws \MWException
+ */
+ protected function getRawDictionaryContent( Title $dictionaryTitle ) {
+
+ global $wgRequest;
+
+ // This is a hack special-casing the submitting of the
terminology page
+ // itself. In this case the Revision is not up to date when we
get here,
+ // i.e. $rev->getText() would return outdated Text. This hack
takes the
+ // text directly out of the data from the web request.
+ if ( $wgRequest->getVal( 'action', 'view' ) === 'submit' &&
+ $this->getTitleFromText( $wgRequest->getVal( 'title' )
)->getArticleID() === $dictionaryTitle->getArticleID()
+ ) {
+
+ return $wgRequest->getVal( 'wpTextbox1' );
+ }
+
+ $rev = $this->getRevisionFromTitle( $dictionaryTitle );
+
+ if ( $rev !== null ) {
+
+ $content = $rev->getContent();
+
+ if ( is_null( $content ) ) {
+ return '';
+ }
+
+ if ( $content instanceof TextContent ) {
+ return $content->getNativeData();
+ }
+
+ $this->getMessageLog()->addError( wfMessage(
'lingo-notatextpage', $dictionaryTitle->getFullText()
)->inContentLanguage()->text() );
+
+ } else {
+
+ $this->getMessageLog()->addWarning( wfMessage(
'lingo-noterminologypage', $dictionaryTitle->getFullText()
)->inContentLanguage()->text() );
+ }
+
+ return '';
+ }
+
+ /**
* Returns revision of the terms page.
*
* @param Title $title
- * @return Revision
+ * @return null|Revision
*/
- public function getRevision( $title ) {
+ protected function getRevisionFromTitle( Title $title ) {
global $wgexLingoEnableApprovedRevs;
if ( $wgexLingoEnableApprovedRevs ) {
if ( defined( 'APPROVED_REVS_VERSION' ) ) {
- $rev_id = ApprovedRevs::getApprovedRevID(
$title );
- return Revision::newFromId( $rev_id );
- } else {
- wfDebug( 'Support for ApprovedRevs is enabled
in Lingo. But ApprovedRevs was not found.\n' );
+ return $this->getApprovedRevisionFromTitle(
$title );
}
+
+ $this->getMessageLog()->addWarning( wfMessage(
'lingo-noapprovedrevs' )->inContentLanguage()->text() );
}
+ return $this->getLatestRevisionFromTitle( $title );
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @param Title $title
+ * @return null|Revision
+ */
+ protected function getApprovedRevisionFromTitle( Title $title ) {
+ return Revision::newFromId( ApprovedRevs::getApprovedRevID(
$title ) );
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @param Title $title
+ * @return null|Revision
+ */
+ protected function getLatestRevisionFromTitle( Title $title ) {
return Revision::newFromTitle( $title );
+ }
+
+ /**
+ * @param string $line
+ * @param string $term
+ * @param string[] $definitions
+ */
+ protected function queueNextLine( $line, &$term, &$definitions ) {
+
+ $chunks = explode( ':', $line, 2 );
+
+ // found a new definition?
+ if ( count( $chunks ) === 2 ) {
+
+ // wipe the data if it's a totally new term definition
+ if ( !empty( $term ) && count( $definitions ) > 0 ) {
+ $definitions = array();
+ $term = null;
+ }
+
+ $definitions[] = trim( $chunks[ 1 ] );
+ }
+
+ // found a new term?
+ if ( count( $chunks ) >= 1 && strlen( $chunks[ 0 ] ) > 0 ) {
+ $term = trim( substr( $chunks[ 0 ], 1 ) );
+ }
}
/**
@@ -158,9 +270,7 @@
*/
public function purgeCache( WikiPage &$wikipage ) {
- $page = $this->getLingoPage();
-
- if ( !is_null( $wikipage ) && (
$wikipage->getTitle()->getText() === $page ) ) {
+ if ( !is_null( $wikipage ) && (
$wikipage->getTitle()->getText() === $this->getLingoPageName() ) ) {
$this->getLingoParser()->purgeGlossaryFromCache();
}
@@ -178,54 +288,5 @@
*/
public function useCache() {
return true;
- }
-
- /**
- * @throws \MWException
- */
- private function setArticleLines() {
- global $wgRequest;
-
- if ( $this->mArticleLines !== null ) {
- return;
- }
-
- $page = $this->getLingoPage();
-
- // Get Terminology page
- $title = Title::newFromText( $page );
- if ( $title->getInterwiki() ) {
- $this->getMessageLog()->addError( wfMessage(
'lingo-terminologypagenotlocal', $page )->inContentLanguage()->text() );
- return;
- }
-
- // This is a hack special-casing the submitting of the
terminology
- // page itself. In this case the Revision is not up to date
when we get
- // here, i.e. $rev->getText() would return outdated Text.
- // This hack takes the text directly out of the data from the
web request.
- if ( $wgRequest->getVal( 'action', 'view' ) === 'submit'
- && Title::newFromText( $wgRequest->getVal( 'title' )
)->getArticleID() === $title->getArticleID()
- ) {
-
- $content = $wgRequest->getVal( 'wpTextbox1' );
-
- } else {
- $rev = $this->getRevision( $title );
- if ( !$rev ) {
- $this->getMessageLog()->addWarning( wfMessage(
'lingo-noterminologypage', $page )->inContentLanguage()->text() );
- return;
- }
-
- $content = ContentHandler::getContentText(
$rev->getContent() );
-
- }
-
- $parser = new Parser;
- // expand templates and variables in the text, producing valid,
static
- // wikitext have to use a new anonymous user to avoid any
leakage as
- // Lingo is caching only one user-independent glossary
- $content = $parser->preprocess( $content, $title, new
ParserOptions( new User() ) );
-
- $this->mArticleLines = array_reverse( explode( "\n", $content )
);
}
}
diff --git a/tests/phpunit/Unit/BasicBackendTest.php
b/tests/phpunit/Unit/BasicBackendTest.php
index 368575e..8f30098 100644
--- a/tests/phpunit/Unit/BasicBackendTest.php
+++ b/tests/phpunit/Unit/BasicBackendTest.php
@@ -62,27 +62,27 @@
$title = $this->getMock( 'Title' );
- $wikiPage = $this->getMockBuilder( 'WikiPage')
+ $wikiPage = $this->getMockBuilder( 'WikiPage' )
->disableOriginalConstructor()
->getMock();
- $lingoParser = $this->getMock( 'Lingo\LingoParser');
+ $lingoParser = $this->getMock( 'Lingo\LingoParser' );
$testObject = $this->getMockBuilder( 'Lingo\BasicBackend' )
- ->setMethods( array( 'getLingoParser') )
+ ->setMethods( array( 'getLingoParser' ) )
->getMock();
- // Assert that the wikipage is tested against the wgexLingoPage:
- // $wikipage->getTitle()->getText() === $page
+ // Assert that the wikipage is tested against the
wgexLingoPage, i.e.
+ // that $wikipage->getTitle()->getText() === $page is tested
$wikiPage->expects( $this->once() )
->method( 'getTitle' )
->willReturn( $title );
$title->expects( $this->once() )
- -> method( 'getText' )
- -> willReturn( 'SomePage' );
+ ->method( 'getText' )
+ ->willReturn( 'SomePage' );
// Assert that purgeGlossaryFromCache is called
$lingoParser->expects( $this->once() )
@@ -90,8 +90,8 @@
$testObject->expects( $this->once() )
- -> method( 'getLingoParser' )
- -> willReturn( $lingoParser );
+ ->method( 'getLingoParser' )
+ ->willReturn( $lingoParser );
$this->assertTrue( $testObject->purgeCache( $wikiPage ) );
}
@@ -104,4 +104,281 @@
$this->assertTrue( $backend->useCache() );
}
+ /**
+ * @covers ::next
+ * @dataProvider provideForTestNext
+ */
+ public function testNext( $lingoPageText, $expectedResults ) {
+
+ $backend = $this->getTestObject( $lingoPageText );
+ foreach ( $expectedResults as $expected ) {
+ $this->assertEquals( $expected, $backend->next() );
+ }
+ }
+
+ public function testNext_LingoPageIsInterwiki() {
+
+ $backend = $this->getTestObject( ';SOT:Some old text', 'view',
'someInterwiki' );
+ $backend->getMessageLog()->expects( $this->once() )
+ ->method( 'addError' )
+ ->willReturn( null );
+
+ $this->assertNull( $backend->next() );
+ }
+
+ public function testNext_LingoPageWasJustEdited() {
+
+ $backend = $this->getTestObject( ';SOT:Some old text', 'submit'
);
+ $this->assertEquals( array( 'JST', 'Just saved text', null,
null ), $backend->next() );
+ }
+
+ public function testNext_LingoPageDoesNotExist() {
+
+ $backend = $this->getTestObject( ';SOT:Some old text', 'view',
'', null, false );
+ $backend->getMessageLog()->expects( $this->once() )
+ ->method( 'addWarning' )
+ ->willReturn( null );
+
+ $this->assertEquals( null, $backend->next() );
+ }
+
+ public function testNext_LingoPageNotAccessible() {
+
+ $backend = $this->getTestObject( ';SOT:Some old text', 'view',
'', false, null );
+ $this->assertEquals( null, $backend->next() );
+ }
+
+ public function testNext_LingoPageIsNotATextPage() {
+
+ $backend = $this->getTestObject( ';SOT:Some old text', 'view',
'', false, 'This is not a TextContent object' );
+ $backend->getMessageLog()->expects( $this->once() )
+ ->method( 'addError' )
+ ->willReturn( null );
+
+ $this->assertEquals( null, $backend->next() );
+ }
+
+ public function testNext_ApprovedRevsEnabledButNotInstalled() {
+
+ $backend = $this->getTestObject( ';SOT:Some old text', 'view',
'', false, false, ';SAT:Some approved text' );
+ $backend->getMessageLog()->expects( $this->once() )
+ ->method( 'addWarning' )
+ ->willReturn( null );
+
+ $GLOBALS[ 'wgexLingoEnableApprovedRevs' ] = true;
+
+ $this->assertEquals( array( 'SOT', 'Some old text', null, null
), $backend->next() );
+ }
+
+ public function testNext_ApprovedRevsEnabledAndInstalled() {
+
+ $backend = $this->getTestObject( ';SOT:Some old text', 'view',
'', false, false, ';SAT:Some approved text' );
+
+ $GLOBALS[ 'wgexLingoEnableApprovedRevs' ] = true;
+ define( 'APPROVED_REVS_VERSION', '42' );
+
+ $this->assertEquals( array( 'SAT', 'Some approved text', null,
null ), $backend->next() );
+ }
+
+
+ /**
+ * @return array
+ */
+ public function provideForTestNext() {
+ return array(
+
+ // Empty page
+ array(
+ '',
+ array( null )
+ ),
+
+ // Simple entries
+ array(
+<<<'TESTTEXT'
+;CIP:Common image point
+;CMP:Common midpoint
+TESTTEXT
+ ,
+ array(
+ array( 'CMP', 'Common midpoint', null,
null ),
+ array( 'CIP', 'Common image point',
null, null ),
+ ),
+ ),
+
+ // Simple entries with line break
+ array(
+<<<'TESTTEXT'
+;CIP
+:Common image point
+;CMP
+:Common midpoint
+TESTTEXT
+ ,
+ array(
+ array( 'CMP', 'Common midpoint', null,
null ),
+ array( 'CIP', 'Common image point',
null, null ),
+ ),
+ ),
+
+ // Two terms having the same definition
+ array(
+<<<'TESTTEXT'
+;CIP
+;CMP
+:Common midpoint
+TESTTEXT
+ ,
+ array(
+ array( 'CMP', 'Common midpoint', null,
null ),
+ array( 'CIP', 'Common midpoint', null,
null ),
+ ),
+ ),
+
+ // One term having two definitions
+ array(
+<<<'TESTTEXT'
+;CIP
+:Common image point
+:Common midpoint
+TESTTEXT
+ ,
+ array(
+ array( 'CIP', 'Common image point',
null, null ),
+ array( 'CIP', 'Common midpoint', null,
null ),
+ ),
+ ),
+
+ // Two terms sharing two definitions
+ array(
+<<<'TESTTEXT'
+;CIP
+;CMP
+:Common image point
+:Common midpoint
+TESTTEXT
+ ,
+ array(
+ array( 'CMP', 'Common image point',
null, null ),
+ array( 'CMP', 'Common midpoint', null,
null ),
+ array( 'CIP', 'Common image point',
null, null ),
+ array( 'CIP', 'Common midpoint', null,
null ),
+ ),
+ ),
+
+ // Mixed entries and noise
+ array(
+<<<'TESTTEXT'
+;CIP:Common image point
+;CMP:Common midpoint
+
+;DIMO
+;DMO
+:Dip move-out
+
+== headline ==
+Sed ut perspiciatis unde; omnis iste natus error: sit voluptatem accusantium...
+
+;NMO:Normal move-out
+TESTTEXT
+ ,
+ array(
+ array( 'NMO', 'Normal move-out', null,
null ),
+ array( 'DMO', 'Dip move-out', null,
null ),
+ array( 'DIMO', 'Dip move-out', null,
null ),
+ array( 'CMP', 'Common midpoint', null,
null ),
+ array( 'CIP', 'Common image point',
null, null ),
+ ),
+ ),
+
+ );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getTestObject( $lingoPageText = '', $action =
'view', $interwiki = '', $lingoPageRevision = false, $lingoPageContent = false,
$lingoApprovedText = '' ) {
+ $messageLog = $this->getMock( 'Lingo\MessageLog' );
+ $messageLogRef =& $messageLog;
+
+ $backend = $this->getMockBuilder( 'Lingo\BasicBackend' )
+ ->disableOriginalConstructor()
+ ->setMethods( array(
+ 'getLatestRevisionFromTitle',
+ 'getApprovedRevisionFromTitle',
+ 'getTitleFromText',
+ ) )
+ ->getMock();
+
+ $reflected = new \ReflectionClass( '\Lingo\BasicBackend' );
+ $constructor = $reflected->getConstructor();
+ $constructor->invoke( $backend, $messageLogRef );
+
+ $GLOBALS[ 'wgLingoPageName' ] = 'SomePage';
+
+ $lingoPageTitle = $this->getMock( 'Title' );
+ $lingoPageTitle->expects( $this->once() )
+ ->method( 'getInterwiki' )
+ ->willReturn( $interwiki );
+ $lingoPageTitle->expects( $this->any() )
+ ->method( 'getArticleID' )
+ ->willReturn( 'Foom' );
+
+ $backend->expects( $this->any() )
+ ->method( 'getTitleFromText' )
+ ->willReturn( $lingoPageTitle );
+
+ $request = $this->getMock( 'FauxRequest' );
+ $request->expects( $this->any() )
+ ->method( 'getVal' )
+ ->willReturnMap( array(
+ array( 'action', 'view', $action ), // action =
submit
+ array( 'title', null, $lingoPageTitle ), //
title = $lingoPageTitle
+ array( 'wpTextbox1', null, ';JST:Just saved
text' )
+ ) );
+
+ $GLOBALS[ 'wgRequest' ] = $request;
+
+ unset( $GLOBALS[ 'wgexLingoEnableApprovedRevs' ] );
+
+ $backend->expects( $this->any() )
+ ->method( 'getLatestRevisionFromTitle' )
+ ->willReturn( $this->getRevisionMock( $lingoPageText,
$lingoPageRevision, $lingoPageContent ) );
+
+ $backend->expects( $this->any() )
+ ->method( 'getApprovedRevisionFromTitle' )
+ ->willReturn( $this->getRevisionMock(
$lingoApprovedText ) );
+
+ return $backend;
+ }
+
+ /**
+ * @param $lingoPageText
+ * @param $lingoPageRevision
+ * @param $lingoPageContent
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getRevisionMock( $lingoPageText, $lingoPageRevision
= false, $lingoPageContent = false ) {
+ if ( $lingoPageRevision === false ) {
+
+ if ( $lingoPageContent === false ) {
+ $lingoPageContent = $this->getMockBuilder(
'TextContent' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $lingoPageContent->expects( $this->any() )
+ ->method( 'getNativeData' )
+ ->willReturn( $lingoPageText );
+ }
+
+ $lingoPageRevision = $this->getMockBuilder( 'Revision' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $lingoPageRevision->expects( $this->any() )
+ ->method( 'getContent' )
+ ->willReturn( $lingoPageContent );
+ return $lingoPageRevision;
+ }
+ return $lingoPageRevision;
+ }
+
}
--
To view, visit https://gerrit.wikimedia.org/r/278682
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I5ffeb4d4667a14349616b13c400540a9eac5d7c7
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Lingo
Gerrit-Branch: master
Gerrit-Owner: Foxtrott <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits