Mooeypoo has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/372572 )
Change subject: RCFilters: Convert saved queries from filters to parameters ...................................................................... RCFilters: Convert saved queries from filters to parameters This will allow us to load them in the backend, and to keep consistency between RecentChanges and Watchlist if needed. Bug: T166908 Change-Id: I8e26b66e43bd16282b7bdb52abc152f92a9c877d --- M resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js M resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js M resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js M resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js M resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js M resources/src/mediawiki.rcfilters/mw.rcfilters.init.js 6 files changed, 253 insertions(+), 192 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/72/372572/1 diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js index 6acc44d..5a2d9b1 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js @@ -812,6 +812,19 @@ }; /** + * Check whether this group is represented by a single parameter + * or whether each item is its own parameter + * + * @return {boolean} This group is a single parameter + */ + mw.rcfilters.dm.FilterGroup.prototype.isGroupParameter = function () { + return ( + this.getType() === 'string_options' || + this.getType() === 'single_option' + ); + }; + + /** * Get display group * * @return {string} Display group diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js index cf226da..6963160 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -599,6 +599,31 @@ }; /** + * Get the parameter names that represent filters that are excluded + * from saved queries. + * + * @return {string[]} Parameter names + */ + mw.rcfilters.dm.FiltersViewModel.prototype.getExcludedParams = function () { + var result = []; + + $.each( this.groups, function ( name, model ) { + if ( model.isExcludedFromSavedQueries() ) { + if ( model.isGroupParameter() ) { + result.push( name ); + } else { + // Each filter is its own param + result.concat( model.getItems().map( function ( filterItem ) { + return filterItem.getParamName(); + } ) ); + } + } + } ); + + return result; + }; + + /** * Analyze the groups and their filters and output an object representing * the state of the parameters they represent. * diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js index a7f3d23..91b0d4b 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js @@ -8,9 +8,10 @@ * * @constructor * @param {Object} [config] Configuration options + * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model * @cfg {string} [default] Default query ID */ - mw.rcfilters.dm.SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( config ) { + mw.rcfilters.dm.SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( config, filtersModel ) { config = config || {}; // Mixin constructor @@ -18,6 +19,7 @@ OO.EmitterList.call( this ); this.default = config.default; + this.filtersModel = filtersModel; // Events this.aggregate( { update: 'itemUpdate' } ); @@ -50,6 +52,9 @@ * Initialize the saved queries model by reading it from the user's settings. * The structure of the saved queries is: * { + * version: (string) Version number; if version 2, the query represents + * parameters. Otherwise, the older version represented filters + * and needs to be readjusted, * default: (string) Query ID * queries:{ * query_id_1: { @@ -70,56 +75,163 @@ * the data * @fires initialize */ - mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries, baseState, ignoreFilters ) { - var items = [], - defaultItem = null; + mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries ) { + var model = this, + excludedParams = this.filtersModel.getExcludedParams() || {}; savedQueries = savedQueries || {}; - ignoreFilters = ignoreFilters || {}; - - this.baseState = baseState; this.clearItems(); - $.each( savedQueries.queries || {}, function ( id, obj ) { - var item, - normalizedData = $.extend( true, {}, baseState, obj.data ), - isDefault = String( savedQueries.default ) === String( id ); - // Backwards-compat fix: We stored the 'highlight' state with - // "1" and "0" instead of true/false; for already-stored states, - // we need to fix that. - // NOTE: Since this feature is only available in beta, we should - // not need this line when we release this to the general wikis. - // This method will automatically fix all saved queries anyways - // for existing users, who are only betalabs users at the moment. - normalizedData.highlights.highlight = !!Number( normalizedData.highlights.highlight ); - - // Backwards-compat fix: Remove sticky parameters from the 'ignoreFilters' list - ignoreFilters.forEach( function ( name ) { - delete normalizedData.filters[ name ]; + if ( savedQueries.version !== '2' ) { + // Old version dealt with filter names. We need to migrate to the new structure + // The new structure: + // { + // version: (stirng) '2', + // default: (string) Query ID, + // queries: { + // query_id: { + // label: (string) Name of the query + // data: { + // params: (object) Representing all the parameter states + // highlights: (object) Representing all the filter highlight states + // } + // } + // } + $.each( savedQueries.queries || {}, function ( id, obj ) { + if ( obj.data && obj.data.filters ) { + obj.data = model.convertToParameters( obj.data ); + } } ); - item = new mw.rcfilters.dm.SavedQueryItemModel( - id, - obj.label, - normalizedData, - { 'default': isDefault } - ); - - if ( isDefault ) { - defaultItem = item; - } - - items.push( item ); - } ); - - if ( defaultItem ) { - this.default = defaultItem.getID(); + savedQueries.version = '2'; } - this.addItems( items ); + // Initialize the query items + $.each( savedQueries.queries || {}, function ( id, obj ) { + var normalizedData = obj.data, + isDefault = String( savedQueries.default ) === String( id ); + + if ( obj.data && obj.data.filters ) { + // Backwards-compat fix: Remove sticky parameters from the 'ignoreFilters' list + excludedParams.forEach( function ( name ) { + delete normalizedData.filters[ name ]; + } ); + + model.addNewQuery( obj.label, normalizedData, isDefault, id ); + + if ( isDefault ) { + model.default = id; + } + } + } ); this.emit( 'initialize' ); + }; + + /** + * Convert from representation of filters to representation of parameters + * + * @param {Object} data Query data + * @return {Object} New converted query data + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.convertToParameters = function ( data ) { + // Filters + data.filters = this.filtersModel.getParametersFromFilters( data.filters ); + + // Highlights + $.each( data.highlights, function ( filterName, color ) { + data.highlights[ filterName + '_color' ] = color; + } ); + + // Highlight toggle + data.highlight = String( Number( data.highlights.highlight ) ); + // Invert toggle + data.invert = String( Number( data.invert || 0 ) ); + + return data; + }; + + /** + * Get an object representing the base state of parameters + * and highlights. + * + * This is meant to make sure that the saved queries that are + * in memory are always the same structure as what we would get + * by calling the current model's "getSelectedState" and by checking + * highlight items. + * + * In cases where a user saved a query when the system had a certain + * set of filters, and then a filter was added to the system, we want + * to make sure that the stored queries can still be comparable to + * the current state, which means that we need the base state for + * two operations: + * + * - Saved queries are stored in "minimal" view (only changed filters + * are stored); When we initialize the system, we merge each minimal + * query with the base state (using 'getNormalizedFilters') so all + * saved queries have the exact same structure as what we would get + * by checking the getSelectedState of the filter. + * - When we save the queries, we minimize the object to only represent + * whatever has actually changed, rather than store the entire + * object. To check what actually is different so we can store it, + * we need to obtain a base state to compare against, this is + * what #getMinimalParamList does + * + * @return {Object} Base parameter state + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.getBaseParamState = function () { + var allParams, + highlightedItems = {}; + + if ( !this.baseParamState ) { + allParams = this.filtersModel.getParametersFromFilters( {} ); + + // Prepare highlights + this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) { + highlightedItems[ item.getName() + '_color' ] = null; + } ); + + this.baseParamState = { + filters: allParams, + highlights: highlightedItems, + highlight: '0', + invert: '0' + }; + } + + return this.baseParamState; + }; + + /** + * Get an object that holds only the parameters and highlights that have + * values different than the base default value. + * + * This is the reverse of the normalization we do initially on loading and + * initializing the saved queries model. + * + * @param {Object} valuesObject Object representing the state of both + * filters and highlights in its normalized version, to be minimized. + * @return {Object} Minimal filters and highlights list + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.getMinimalParamList = function ( valuesObject ) { + var result = { filters: {}, highlights: {}, invert: valuesObject.invert, highlight: valuesObject.highlight }, + baseState = this.getBaseParamState(); + + // XOR results + $.each( valuesObject.filters, function ( name, value ) { + if ( baseState.filters !== undefined && baseState.filters[ name ] !== value ) { + result.filters[ name ] = value; + } + } ); + + $.each( valuesObject.highlights, function ( name, value ) { + if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) { + result.highlights[ name ] = value; + } + } ); + + return result; }; /** @@ -127,18 +239,22 @@ * * @param {string} label Label for the new query * @param {Object} data Data for the new query + * @param {boolean} isDefault Item is default + * @param {string} [id] Query ID, if exists. If this isn't given, a random + * new ID will be created. * @return {string} ID of the newly added query */ - mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, data ) { - var randomID = ( new Date() ).getTime(), - normalizedData = $.extend( true, {}, this.baseState, data ); + mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, data, isDefault, id ) { + var randomID = id || ( new Date() ).getTime(), + normalizedData = this.getMinimalParamList( data ); // Add item this.addItems( [ new mw.rcfilters.dm.SavedQueryItemModel( randomID, label, - normalizedData + normalizedData, + { 'default': isDefault } ) ] ); @@ -171,6 +287,9 @@ * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model */ mw.rcfilters.dm.SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) { + // Minimize before comparison + fullQueryComparison = this.getMinimalParamList( fullQueryComparison ); + return this.getItems().filter( function ( item ) { return OO.compare( item.getData(), @@ -193,17 +312,32 @@ }; /** + * Get an item's full data + * + * @param {string} queryID Query identifier + * @return {Object} Item's full data + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.getItemFullData = function ( queryID ) { + var item = this.getItemByID( queryID ); + + // Fill in the base params + return item ? $.extend( true, {}, this.getBaseParamState(), item.getData() ) : {}; + }; + /** * Get the object representing the state of the entire model and items * * @return {Object} Object representing the state of the model and items */ mw.rcfilters.dm.SavedQueriesModel.prototype.getState = function () { - var obj = { queries: {} }; + var model = this, + obj = { queries: {}, version: '2' }; // Translate the items to the saved object this.getItems().forEach( function ( item ) { var itemState = item.getState(); + itemState.data = model.getMinimalParamList( itemState.data ); + obj.queries[ item.getID() ] = itemState; } ); diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js index 0085bd6..6c27ea4 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js @@ -202,8 +202,6 @@ // Initialize the model this.filtersModel.initializeFilters( filterStructure, views ); - this._buildBaseFilterState(); - this.uriProcessor = new mw.rcfilters.UriProcessor( this.filtersModel ); @@ -217,12 +215,7 @@ // The queries are saved in a minimized state, so we need // to send over the base state so the saved queries model // can normalize them per each query item - this.savedQueriesModel.initialize( - parsedSavedQueries, - this._getBaseFilterState(), - // This is for backwards compatibility - delete all excluded filter states - Object.keys( this.filtersModel.getExcludedFiltersState() ) - ); + this.savedQueriesModel.initialize( parsedSavedQueries ); // Check whether we need to load defaults. // We do this by checking whether the current URI query @@ -584,35 +577,30 @@ * @param {boolean} [setAsDefault=false] This query should be set as the default */ mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) { - var queryID, - highlightedItems = {}, + var highlightedItems = {}, highlightEnabled = this.filtersModel.isHighlightEnabled(), selectedState = this.filtersModel.getSelectedState(); // Prepare highlights this.filtersModel.getHighlightedItems().forEach( function ( item ) { - highlightedItems[ item.getName() ] = highlightEnabled ? + highlightedItems[ item.getName() + '_color' ] = highlightEnabled ? item.getHighlightColor() : null; } ); - // These are filter states; highlight is stored as boolean - highlightedItems.highlight = this.filtersModel.isHighlightEnabled(); // Delete all excluded filters this._deleteExcludedValuesFromFilterState( selectedState ); // Add item - queryID = this.savedQueriesModel.addNewQuery( + this.savedQueriesModel.addNewQuery( label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ), { - filters: selectedState, + filters: this.filtersModel.getParametersFromFilters( selectedState ), highlights: highlightedItems, - invert: this.filtersModel.areNamespacesInverted() - } + invert: String( Number( this.filtersModel.areNamespacesInverted() ) ), + highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ) + }, + setAsDefault ); - - if ( setAsDefault ) { - this.savedQueriesModel.setDefault( queryID ); - } // Save item this._saveSavedQueries(); @@ -661,8 +649,9 @@ * @param {string} queryID Query id */ mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) { - var data, highlights, + var highlights, queryItem = this.savedQueriesModel.getItemByID( queryID ), + data = this.savedQueriesModel.getItemFullData( queryID ), currentMatchingQuery = this.findQueryMatchingCurrentState(); if ( @@ -674,25 +663,26 @@ currentMatchingQuery.getID() !== queryItem.getID() ) ) { - data = queryItem.getData(); highlights = data.highlights; - - // Backwards compatibility; initial version mispelled 'highlight' with 'highlights' - highlights.highlight = highlights.highlights || highlights.highlight; // Update model state from filters this.filtersModel.toggleFiltersSelected( // Merge filters with excluded values - $.extend( true, {}, data.filters, this.filtersModel.getExcludedFiltersState() ) + $.extend( + true, + {}, + this.filtersModel.getFiltersFromParameters( data.filters ), + this.filtersModel.getExcludedFiltersState() + ) ); // Update namespace inverted property this.filtersModel.toggleInvertedNamespaces( !!Number( data.invert ) ); // Update highlight state - this.filtersModel.toggleHighlight( !!Number( highlights.highlight ) ); + this.filtersModel.toggleHighlight( !!Number( data.highlight ) ); this.filtersModel.getItems().forEach( function ( filterItem ) { - var color = highlights[ filterItem.getName() ]; + var color = highlights[ filterItem.getName() + '_color' ]; if ( color ) { filterItem.setHighlightColor( color ); } else { @@ -722,9 +712,8 @@ // Prepare highlights of the current query this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) { - highlightedItems[ item.getName() ] = item.getHighlightColor(); + highlightedItems[ item.getName() + '_color' ] = item.getHighlightColor(); } ); - highlightedItems.highlight = this.filtersModel.isHighlightEnabled(); // Remove anything that should be excluded from the saved query // this includes sticky filters and filters marked with 'excludedFromSavedQueries' @@ -732,9 +721,10 @@ return this.savedQueriesModel.findMatchingQuery( { - filters: selectedState, + filters: this.filtersModel.getParametersFromFilters( selectedState ), highlights: highlightedItems, - invert: this.filtersModel.areNamespacesInverted() + highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ), + invert: String( Number( this.filtersModel.areNamespacesInverted() ) ) } ); }; @@ -752,112 +742,12 @@ }; /** - * Get an object representing the base state of parameters - * and highlights. - * - * This is meant to make sure that the saved queries that are - * in memory are always the same structure as what we would get - * by calling the current model's "getSelectedState" and by checking - * highlight items. - * - * In cases where a user saved a query when the system had a certain - * set of filters, and then a filter was added to the system, we want - * to make sure that the stored queries can still be comparable to - * the current state, which means that we need the base state for - * two operations: - * - * - Saved queries are stored in "minimal" view (only changed filters - * are stored); When we initialize the system, we merge each minimal - * query with the base state (using 'getNormalizedFilters') so all - * saved queries have the exact same structure as what we would get - * by checking the getSelectedState of the filter. - * - When we save the queries, we minimize the object to only represent - * whatever has actually changed, rather than store the entire - * object. To check what actually is different so we can store it, - * we need to obtain a base state to compare against, this is - * what #_getMinimalFilterList does - */ - mw.rcfilters.Controller.prototype._buildBaseFilterState = function () { - var defaultParams = this.filtersModel.getDefaultParams(), - highlightedItems = {}; - - // Prepare highlights - this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) { - highlightedItems[ item.getName() ] = null; - } ); - highlightedItems.highlight = false; - - this.baseFilterState = { - filters: this.filtersModel.getFiltersFromParameters( defaultParams ), - highlights: highlightedItems, - invert: false - }; - }; - - /** - * Get an object representing the base filter state of both - * filters and highlights. The structure is similar to what we use - * to store each query in the saved queries object: - * { - * filters: { - * filterName: (bool) - * }, - * highlights: { - * filterName: (string|null) - * } - * } - * - * @return {Object} Object representing the base state of - * parameters and highlights - */ - mw.rcfilters.Controller.prototype._getBaseFilterState = function () { - return this.baseFilterState; - }; - - /** - * Get an object that holds only the parameters and highlights that have - * values different than the base default value. - * - * This is the reverse of the normalization we do initially on loading and - * initializing the saved queries model. - * - * @param {Object} valuesObject Object representing the state of both - * filters and highlights in its normalized version, to be minimized. - * @return {Object} Minimal filters and highlights list - */ - mw.rcfilters.Controller.prototype._getMinimalFilterList = function ( valuesObject ) { - var result = { filters: {}, highlights: {}, invert: valuesObject.invert }, - baseState = this._getBaseFilterState(); - - // XOR results - $.each( valuesObject.filters, function ( name, value ) { - if ( baseState.filters !== undefined && baseState.filters[ name ] !== value ) { - result.filters[ name ] = value; - } - } ); - - $.each( valuesObject.highlights, function ( name, value ) { - if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) { - result.highlights[ name ] = value; - } - } ); - - return result; - }; - - /** * Save the current state of the saved queries model with all * query item representation in the user settings. */ mw.rcfilters.Controller.prototype._saveSavedQueries = function () { var stringified, - state = this.savedQueriesModel.getState(), - controller = this; - - // Minimize before save - $.each( state.queries, function ( queryID, info ) { - state.queries[ queryID ].data = controller._getMinimalFilterList( info.data ); - } ); + state = this.savedQueriesModel.getState(); // Stringify state stringified = JSON.stringify( state ); @@ -1034,22 +924,19 @@ * @return {Object} Default parameters */ mw.rcfilters.Controller.prototype._getDefaultParams = function () { - var data, queryHighlights, + var queryHighlights, savedParams = {}, savedHighlights = {}, - defaultSavedQueryItem = this.savedQueriesModel.getItemByID( this.savedQueriesModel.getDefault() ); + stickyParams = this.filtersModel.getParametersFromFilters( this.filtersModel.getStickyFiltersState() ), + data = this.savedQueriesModel.getItemFullData( this.savedQueriesModel.getDefault() ); - if ( defaultSavedQueryItem ) { - data = defaultSavedQueryItem.getData(); - - queryHighlights = data.highlights || {}; - savedParams = this.filtersModel.getParametersFromFilters( - // Merge filters with sticky values - $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() ) - ); + if ( !$.isEmptyObject( data ) ) { + // Merge filters with sticky values + savedParams = $.extend( true, {}, data.filters, stickyParams ); // Translate highlights to parameters - savedHighlights.highlight = String( Number( queryHighlights.highlight ) ); + queryHighlights = data.highlights || {}; + savedHighlights.highlight = queryHighlights.highlight; $.each( queryHighlights, function ( filterName, color ) { if ( filterName !== 'highlights' ) { savedHighlights[ filterName + '_color' ] = color; @@ -1058,7 +945,6 @@ return $.extend( true, {}, savedParams, savedHighlights ); } - return this.filtersModel.getDefaultParams(); }; diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js index a1ef981..17bef74 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js @@ -310,4 +310,7 @@ { highlight: '0', invert: '0' } ); }; + mw.rcfilters.UriProcessor.prototype.getEmptyParameterState = function () { + return this.emptyParameterState; + }; }( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js index 701e61d..a2c0e28 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js @@ -14,7 +14,7 @@ topLinksCookieValue = topLinksCookie || 'collapsed', filtersModel = new mw.rcfilters.dm.FiltersViewModel(), changesListModel = new mw.rcfilters.dm.ChangesListViewModel(), - savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel(), + savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel( {}, filtersModel ), controller = new mw.rcfilters.Controller( filtersModel, changesListModel, savedQueriesModel ), $overlay = $( '<div>' ) .addClass( 'mw-rcfilters-ui-overlay' ), -- To view, visit https://gerrit.wikimedia.org/r/372572 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I8e26b66e43bd16282b7bdb52abc152f92a9c877d Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Mooeypoo <mor...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits