MaxSem has uploaded a new change for review.
https://gerrit.wikimedia.org/r/90770
Change subject: Port to express
......................................................................
Port to express
Change-Id: I9e574a990f3401d8005aab2b9bd1534f51a8f186
---
A server/app.js
D server/config.js
A server/lib/config.js
A server/lib/request.js
A server/lib/server.js
R server/lib/stats-ui.js
A server/lib/stats.js
A server/lib/work.js
M server/package.json
A server/public/stylesheets/style.css
D server/request.js
A server/routes/index.js
A server/routes/minify.js
D server/server.js
D server/utils.js
A server/views/index.jade
A server/views/layout.jade
D server/work.js
M server/worker.js
19 files changed, 381 insertions(+), 330 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Minifier
refs/changes/70/90770/1
diff --git a/server/app.js b/server/app.js
new file mode 100644
index 0000000..334fa56
--- /dev/null
+++ b/server/app.js
@@ -0,0 +1,45 @@
+/**
+ * JavaScript minification server
+ * API:
+ * GET / - returns statistics
+ * POST / - returns minified JS. Parameters:
+ * - text - JS to minify
+ * - id - cache ID for this JS chunk [optional]
+ */
+
+var config = require( './lib/config' );
+config.load( 'main', 'settings.json' );
+
+var express = require( 'express' ),
+ routes = require( './routes' ),
+ minify = require( './routes/minify' ),
+ http = require( 'http' ),
+ path = require( 'path' ),
+ server = require( './lib/server' );
+
+
+var app = express();
+server.init();
+
+// all environments
+app.set( 'port', config.main.network.port );
+app.set( 'views', __dirname + '/views' );
+app.set( 'view engine', 'jade' );
+app.use( express.favicon() );
+app.use( express.logger( 'dev') );
+app.use( express.bodyParser() );
+app.use( express.methodOverride() );
+app.use( app.router );
+app.use( express.static( path.join( __dirname, 'public' ) ) );
+
+// development only
+if ( 'development' == app.get( 'env' ) ) {
+ app.use( express.errorHandler() );
+}
+
+app.get( '/', routes.index );
+app.post( '/', minify.post );
+
+http.createServer( app ).listen( app.get( 'port' ), function() {
+ console.log( 'Express server listening on port ' + app.get( 'port' ) );
+});
diff --git a/server/config.js b/server/config.js
deleted file mode 100644
index 0b9a49a..0000000
--- a/server/config.js
+++ /dev/null
@@ -1,32 +0,0 @@
-var fs = require('fs');
-var util = require('util');
-
-function Config() {
- this.files = {};
- this.verbose = true;
-}
-
-Config.prototype = {
- load: function(name, file) {
- this.log('Loading configuration file ' + file);
- this.files[name] = file;
- var json = fs.readFileSync(file, {encoding: 'ascii'});
- this[name] = JSON.parse(json);
- return this[name];
- },
-
- reload: function() {
- this.log('Reloading configuration');
- for (var name in this.files) {
- this.load(name, this.files[name]);
- }
- },
-
- log: function(line) {
- if (this.verbose) {
- console.log(line);
- }
- }
-};
-
-module.exports = new Config;
diff --git a/server/lib/config.js b/server/lib/config.js
new file mode 100644
index 0000000..4c17507
--- /dev/null
+++ b/server/lib/config.js
@@ -0,0 +1,23 @@
+var fs = require('fs' );
+
+function Config() {
+ this.files = {};
+ this.verbose = true;
+}
+
+Config.prototype = {
+ load: function( name, file ) {
+ this.files[name] = file;
+ var json = fs.readFileSync( file, { encoding: 'ascii' } );
+ this[name] = JSON.parse( json );
+ return this[name];
+ },
+
+ reload: function() {
+ for ( var name in this.files ) {
+ this.load( name, this.files[name] );
+ }
+ },
+};
+
+module.exports = new Config;
diff --git a/server/lib/request.js b/server/lib/request.js
new file mode 100644
index 0000000..244e391
--- /dev/null
+++ b/server/lib/request.js
@@ -0,0 +1,44 @@
+module.exports = Request;
+
+var log = require( './server' ).log,
+ config = require( './config' ),
+ stats = require( './stats' );
+
+/**
+ * Class that represents a request waiting for completion
+ * @param callback
+ * @constructor
+ */
+function Request( callback ) {
+ this.callback = callback;
+ this.live = true;
+
+ var self = this;
+ this.timer = setTimeout( function() { self.timeout(); },
config.main.network.timeout );
+}
+
+Request.prototype = {
+ timeout: function() {
+ stats.requestTimeouts++;
+ this.respond( '', 'Timed out' );
+ },
+
+ success: function( text ) {
+ stats.requestsResponded++;
+ this.respond( text );
+ },
+
+ error: function( msg ) {
+ stats.requestErrors++;
+ this.respond( '', msg );
+ },
+
+ respond: function( text, err ) {
+ if ( !this.live ) {
+ return;
+ }
+ clearTimeout( this.timer );
+ this.live = false;
+ this.callback( text, err );
+ }
+};
diff --git a/server/lib/server.js b/server/lib/server.js
new file mode 100644
index 0000000..038fa49
--- /dev/null
+++ b/server/lib/server.js
@@ -0,0 +1,120 @@
+var cluster = require( 'cluster' );
+var os = require( 'os' );
+var lru = require( 'lru-cache' );
+var crypto = require( 'crypto' );
+var config = require( './config' );
+var Work = require( './work' );
+var stats = require( './stats' );
+var Request = require( './request.js' );
+
+function log( str ) {
+ console.log( '[server] ' + str );
+}
+
+function md5( data ) {
+ var hash = crypto.createHash( 'md5' );
+ hash.update( data );
+ return hash.digest( 'hex' );
+}
+
+module.exports = {
+ /**
+ * Sets up multithreading
+ */
+ init: function() {
+ cluster.setupMaster( { exec: '../worker.js' } );
+
+ cluster.on( 'disconnect', function( worker ) {
+ log( 'Worker ' + worker.id + ' has disconnected,
cleaning up and restarting.' );
+ stats.workerErrors++;
+ if ( worker.work ) {
+ worker.work.failed( 'Worker disconnection' );
+ }
+ var newWorker = cluster.fork();
+ setupWorker( newWorker );
+ var work = this.queue.unshift();
+ if ( work ) {
+ work.sendToWorker( worker );
+ }
+ } );
+
+ cluster.on( 'exit', function( worker, code, signal ) {
+ var s = 'Worker ' + worker.id + ' has exited with code
' + code;
+ if ( signal !== null ) {
+ s += ' upon signal ' + signal;
+ }
+ s += '.';
+ log( s );
+ } );
+
+ var cpus = os.cpus().length;
+ log( 'Started, pid=' + process.pid + '. Spawning ' + cpus + '
workers.' );
+ for ( var i = 0; i < cpus; i++ ) {
+ var worker = cluster.fork();
+ this.setupWorker( worker );
+ }
+ },
+
+ log: log,
+
+ cache: lru( {
+ max: config.main.queue.sizeInMB * 1024 * 1024,
+ length: function( s ) { return s.length; }
+ } ),
+
+ queue: [],
+
+ minify: function( id, text, callback ) {
+ stats.requests++;
+ if ( typeof id === 'undefined' ) {
+ id = md5( text );
+ }
+ var cached = this.cache.get( id );
+ if ( typeof cached !== 'undefined' ) {
+ stats.cacheHits++;
+ log( 'Cache hit' );
+ sendMinified( response, cached );
+ } else {
+ var pending = new Request( callback )
+ // @todo: Non-linear search?
+ if ( !this.sendToFreeWorker( id, text, pending ) ) {
+ this.enqueue( id, text, pending )
+ var work = new Work( id, text, pending );
+ }
+ }
+ },
+
+ sendToFreeWorker: function( id, text, pending ) {
+ for ( var workerId in cluster.workers ) {
+ var worker = cluster.workers[workerId];
+ if ( typeof worker.currentWork === 'undefined' ) {
+ worker.currentWork = new Work( id, text,
pending );
+ worker.currentWork.sendToWorker( worker );
+ log( 'Sending work ' + id + ' to worker #' +
workerId );
+ return true;
+ }
+ }
+ return false;
+ },
+
+ enqueue: function( id, text, request ) {
+ for ( var i = 0; i < this.queue.length; i++ ) {
+ if ( this.queue[i].id == id ) {
+ this.queue[i].requests.push( request );
+ return;
+ }
+ }
+ this.queue.push( new Work( id, text, request ) );
+ },
+
+ setupWorker: function( worker ) {
+ worker.on( 'message', function( msg ) {
+ if ( msg.code === 'minified' ) {
+ if ( !worker.currentWork || msg.id !=
worker.currentWork.id ) {
+ log( 'Worker #' + worker.id + ':
unexpected work ' + msg.id );
+ }
+ worker.currentWork.done( msg.text );
+ }
+ } );
+ }
+};
\ No newline at end of file
diff --git a/server/stats.js b/server/lib/stats-ui.js
similarity index 65%
rename from server/stats.js
rename to server/lib/stats-ui.js
index 39cab99..7f45506 100644
--- a/server/stats.js
+++ b/server/lib/stats-ui.js
@@ -1,3 +1,5 @@
+var stats = require( './stats' );
+
var humanNames = {
requests: 'Total requests',
requestErrors: 'Request errors',
@@ -14,32 +16,24 @@
json: { renderer: renderStatsJson, type: 'application/json' }
}
-function renderStats( stats, format ) {
- if ( stats.requestsResponded ) {
- stats.minificationTime /= stats.requestsResponded;
- }
- stats.pendingRequests = stats.requests - stats.requestErrors -
stats.requestsResponded;
-
+function renderStats( format ) {
if ( formats[format] ) {
- return { output: formats[format].renderer( stats ), type:
formats[format].type };
+ return { output: formats[format].renderer(), type:
formats[format].type };
}
return { output: '', type: 'text/html' };
}
-function renderStatsHtml( stats ) {
- var s = '<!doctype html><html><title>Minifier server
statistics</title><body>';
-
- s += '<table>';
+function renderStatsHtml() {
+ var s = '<table>';
for ( var name in humanNames ) {
s += '<tr><td>' + humanNames[name] + '</td><td>' + stats[name]
+ '</td></tr>';
}
s += '</table><hr>';
s += '<form method="POST" action="/"><textarea name="text" rows="10"
cols="80"></textarea><br><input type="submit" value="Minify!"/></form>';
- s += '</body></html>';
return s;
}
-function renderStatsText( stats ) {
+function renderStatsText() {
var s = '';
for ( var name in humanNames ) {
s += humanNames[name] + ': ' + stats[name] + '\n';
@@ -47,7 +41,7 @@
return s;
}
-function renderStatsJson( stats ) {
+function renderStatsJson() {
return JSON.stringify( stats );
}
diff --git a/server/lib/stats.js b/server/lib/stats.js
new file mode 100644
index 0000000..42a0466
--- /dev/null
+++ b/server/lib/stats.js
@@ -0,0 +1,21 @@
+/**
+ * Server statistics
+ * @type Object
+ */
+module.exports = {
+ // Number of requests received
+ requests: 0,
+
+ // Number of minification requests that received an error response
+ requestErrors: 0,
+ requestsResponded: 0,
+ requestTimeouts: 0,
+ //disconnects: 0,
+ minificationTime: 0,
+ workerErrors: 0,
+ cacheHits: 0,
+ cacheMisses: 0,
+ queueHits: 0,
+ queueMisses: 0,
+ queueLength: 0
+};
diff --git a/server/lib/work.js b/server/lib/work.js
new file mode 100644
index 0000000..4583dfe
--- /dev/null
+++ b/server/lib/work.js
@@ -0,0 +1,28 @@
+function Work( id, text, request ) {
+ this.id = id;
+ this.text = text;
+ this.requests = [request];
+}
+
+Work.prototype = {
+ done: function( text ) {
+ this.respond( 'success', text );
+ },
+
+ failed: function( reason ) {
+ this.respond( 'failed', '', reason );
+ },
+
+ respond: function( method, param1, param2 ) {
+ for ( var i = 0; i < this.requests.length; i++ ) {
+ this.requests[i][method]( param1, param2 );
+ }
+ },
+
+ sendToWorker: function( worker ) {
+ worker.work = this;
+ worker.send( { code: 'minify', id: this.id, text: this.text } );
+ }
+};
+
+module.exports = Work;
diff --git a/server/package.json b/server/package.json
index ac48216..5c1f673 100644
--- a/server/package.json
+++ b/server/package.json
@@ -2,8 +2,14 @@
"name": "minifier-server",
"description": "UglifyJS server for MediaWiki",
"version": "0.5.0",
+ "private": true,
+ "scripts": {
+ "start": "node app.js"
+ },
"dependencies": {
"lru-cache": "2.x.x",
- "uglify-js": "2.x.x"
+ "uglify-js": "2.x.x",
+ "express": "3.3.4",
+ "jade": "*"
}
-}
+}
\ No newline at end of file
diff --git a/server/public/stylesheets/style.css
b/server/public/stylesheets/style.css
new file mode 100644
index 0000000..30e047d
--- /dev/null
+++ b/server/public/stylesheets/style.css
@@ -0,0 +1,8 @@
+body {
+ padding: 50px;
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+}
+
+a {
+ color: #00B7FF;
+}
\ No newline at end of file
diff --git a/server/request.js b/server/request.js
deleted file mode 100644
index d1cba49..0000000
--- a/server/request.js
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-module.exports = PendingRequest;
\ No newline at end of file
diff --git a/server/routes/index.js b/server/routes/index.js
new file mode 100644
index 0000000..35f38d0
--- /dev/null
+++ b/server/routes/index.js
@@ -0,0 +1,14 @@
+/*
+ * GET home page.
+ */
+
+var stats = require( '../lib/stats' );
+
+exports.index = function( req, res ) {
+ if ( stats.requestsResponded ) {
+ stats.minificationTime /= stats.requestsResponded;
+ }
+ stats.pendingRequests = stats.requests - stats.requestErrors -
stats.requestsResponded;
+
+ res.render( 'index', { title: 'Minification server statistics', stats:
stats } );
+};
\ No newline at end of file
diff --git a/server/routes/minify.js b/server/routes/minify.js
new file mode 100644
index 0000000..22adb2b
--- /dev/null
+++ b/server/routes/minify.js
@@ -0,0 +1,26 @@
+/*
+ * POST to home page - minify
+ */
+
+var server = require( '../lib/server' );
+
+function sendMinified( response, text ) {
+ stats.requestsResponded++;
+ response.writeHead( 200, { 'Content-Type': 'application/javascript' } );
+ response.write( text );
+ response.end();
+}
+
+exports.post = function( req, res ) {
+ var text = req.param( 'text' );
+ var id = req.param( 'id' );
+ console.log(text);
+
+ server.minify( id, text, function( text, err ) {
+ if ( typeof err == 'undefined' ) {
+
+ } else {
+ res.render( 'index', { title: 'Minification server
statistics', body: statsUI.render( 'html' ).output } );
+ }
+ } );
+};
diff --git a/server/server.js b/server/server.js
deleted file mode 100644
index 7400b9c..0000000
--- a/server/server.js
+++ /dev/null
@@ -1,243 +0,0 @@
-/**
- * JavaScript minification server
- * API:
- * GET / - returns statistics
- * POST / - returns minified JS. Parameters:
- * - text - JS to minify
- * - id - cache ID for this JS chunk [optional]
- */
-
-var http = require( 'http' );
-var cluster = require( 'cluster' );
-var os = require( 'os' );
-var url = require( 'url' );
-var queryString = require( 'querystring' );
-var lru = require( 'lru-cache' );
-var crypto = require( 'crypto' );
-
-var config = require( './config' );
-var statsUI = require( './stats' );
-var utils = require( './utils' );
-var Work = require( './work' );
-
-config.load( 'main', 'settings.json' );
-
-/**
- * Server statistics
- * @type Object
- */
-var stats = {
- // Number of requests received
- requests: 0,
-
- // Number of minification requests that received an error response
- requestErrors: 0,
- requestsResponded: 0,
- requestTimeouts: 0,
- //disconnects: 0,
- minificationTime: 0,
- workerErrors: 0,
- cacheHits: 0,
- cacheMisses: 0,
- queueHits: 0,
- queueMisses: 0,
- queueLength: 0
-};
-
-setupCluster();
-
-var cache = lru( {
- max: config.main.queue.sizeInMB * 1024 * 1024,
- length: function( s ) { return s.length; }
-} );
-var queue = [];
-
-http.createServer(
- function ( request, response ) {
- log( request.method + ' ' + request.url + ' from ' +
request.connection.remoteAddress );
- var parsedUrl = url.parse( request.url );
- if ( parsedUrl.pathname === '/' ) {
- if ( request.method === 'GET' ) {
- outputStats( request, response );
- } else if ( request.method === 'POST' ) {
- getBody( request, response );
- } else {
- reportError( response, 405, 'Method Not
Allowed' );
- }
- } else {
- reportError( response, 404, 'Not Found' );
- }
- }
-).listen( config.main.network.port );
-
-function outputStats( request, response ) {
- stats.queueLength = queue.length;
- var res = statsUI.render( stats, 'html' );
- response.writeHead( 200, { 'Content-Type': res.type } );
- response.write( res.output );
- response.end();
-}
-
-function reportError( response, code, name, message ) {
- response.writeHead( code, { 'Content-Type': 'text/html' } );
- response.write( utils.errorPage( code, name, message ) );
- response.end();
-}
-
-function getBody( request, response ) {
- var post = '';
- request.on( 'data', function( data ) {
- post += data;
- } );
- request.on( 'end', function() {
- request.post = queryString.parse( post );
- minify( request, response );
- } );
-}
-
-function minify( request, response ) {
- stats.requests++;
- var text = request.post.text;
- var id = request.post.id;
- if ( typeof id === 'undefined' ) {
- id = md5( text );
- }
- var cached = cache.get( id );
- if ( typeof cached !== 'undefined' ) {
- stats.cacheHits++;
- log('cache hit');
- sendMinified( response, cached );
- } else {
- var pending = new PendingRequest( response )
- // @todo: Non-linear search?
- if ( !sendToFreeWorker( id, text, pending ) ) {
- enqueue( id, text, pending )
- var work = new Work( id, text, pending );
- }
- }
-}
-
-function sendToFreeWorker( id, text, pending ) {
- for ( var workerId in cluster.workers ) {
- var worker = cluster.workers[workerId];
- if ( typeof worker.currentWork === 'undefined' ) {
- worker.currentWork = new Work( id, text, pending );
- worker.currentWork.sendToWorker( worker );
- log( 'Sending work ' + id + ' to worker #' + workerId );
- return true;
- }
- }
- return false;
-}
-
-function enqueue( id, text, request ) {
- for ( var i = 0; i < queue.length; i++ ) {
- if ( queue[i].id == id ) {
- queue[i].requests.push( request );
- return;
- }
- }
- queue.push( new Work( id, text, request ) );
-}
-
-function sendMinified( response, text ) {
- stats.requestsResponded++;
- response.writeHead( 200, { 'Content-Type': 'application/javascript' } );
- response.write( text );
- response.end();
-}
-
-function log( str ) {
- console.log( '[server] ' + str );
-}
-
-function md5( data ) {
- var hash = crypto.createHash( 'md5' );
- hash.update( data );
- return hash.digest( 'hex' );
-}
-
-/**
- * Sets up multithreading
- */
-function setupCluster() {
- cluster.setupMaster( { exec: 'worker.js' } );
-
- cluster.on( 'disconnect', function( worker ) {
- log( 'Worker ' + worker.id + ' has disconnected, cleaning up
and restarting.' );
- stats.workerErrors++;
- if ( worker.work ) {
- worker.work.failed( 'Worker disconnection' );
- }
- var newWorker = cluster.fork();
- setupWorker( newWorker );
- var work = queue.unshift();
- if ( work ) {
- work.sendToWorker( worker );
- }
- } );
-
- cluster.on( 'exit', function( worker, code, signal ) {
- var s = 'Worker ' + worker.id + ' has exited with code ' + code;
- if ( signal !== null ) {
- s += ' upon signal ' + signal;
- }
- s += '.';
- log( s );
- } );
-
- var cpus = os.cpus().length;
- log( 'Started, pid=' + process.pid + '. Spawning ' + cpus + ' workers.'
);
- for ( var i = 0; i < cpus; i++ ) {
- var worker = cluster.fork();
- setupWorker( worker );
- }
-}
-
-function setupWorker( worker ) {
- worker.on( 'message', function( msg ) {
- if ( msg.code === 'minified' ) {
- if ( !worker.currentWork || msg.id !=
worker.currentWork.id ) {
- log( 'Worker #' + worker.id + ': unexpected
work ' + msg.id );
- }
- worker.currentWork.done( msg.text );
- }
- } );
-}
-
-/**
- * Class that represents a request waiting for completion
- * @param response
- * @constructor
- */
-function PendingRequest( response ) {
- this.response = response;
- this.live = true;
-
- var thisSaved = this;
- this.timer = setTimeout( function() { thisSaved.timeout(); },
config.main.network.timeout );
-}
-
-PendingRequest.prototype = {
- timeout: function() {
- stats.requestTimeouts++;
- reportError( this.response, 500, 'Internal Server Error',
'Timed out' );
- log( 'Request timeout' );
- this.destroy();
- },
-
- respond: function( code, type, content ) {
- if ( !this.live ) {
- return;
- }
- this.response.writeHead( code, { 'Content-Type': type } );
- this.response.write( content );
- this.response.end();
- this.destroy();
- },
-
- destroy: function() {
- clearTimeout( this.timer );
- this.live = false;
- }
-};
\ No newline at end of file
diff --git a/server/utils.js b/server/utils.js
deleted file mode 100644
index 34b6afe..0000000
--- a/server/utils.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- errorPage: function( code, name, message ) {
- return '<!doctype html><html><title>Error</title><body><h1>' +
code + ' '
- + name + '</h1>' + message + '</body></html>';
- }
-};
diff --git a/server/views/index.jade b/server/views/index.jade
new file mode 100644
index 0000000..110fe11
--- /dev/null
+++ b/server/views/index.jade
@@ -0,0 +1,28 @@
+extends layout
+
+block content
+ h1= title
+ table
+ tr
+ td Total requests
+ td= stats.requests
+ tr
+ td Request errors
+ td= stats.requestErrors
+ tr
+ td Requests served
+ td= stats.requestsResponded
+ tr
+ td Pending requests
+ td= stats.pendingRequests
+ tr
+ td Average minification time
+ td= stats.minificationTime
+ tr
+ td Worker errors
+ td= stats.workerErrors
+
+ form(method='POST', action='/')
+ textarea(name='text', rows='10', cols='80', placeholder='Try
minification here!')
+ br
+ input(type='submit', value='Minify!')
diff --git a/server/views/layout.jade b/server/views/layout.jade
new file mode 100644
index 0000000..1b7b305
--- /dev/null
+++ b/server/views/layout.jade
@@ -0,0 +1,7 @@
+doctype 5
+html
+ head
+ title= title
+ link(rel='stylesheet', href='/stylesheets/style.css')
+ body
+ block content
\ No newline at end of file
diff --git a/server/work.js b/server/work.js
deleted file mode 100644
index b48148f..0000000
--- a/server/work.js
+++ /dev/null
@@ -1,30 +0,0 @@
-var utils = require( './utils' );
-
-function Work( id, text, request ) {
- this.id = id;
- this.text = text;
- this.requests = [request];
-}
-
-Work.prototype = {
- done: function( text ) {
- this.respond( 200, 'application/javascript', text );
- },
-
- failed: function( reason ) {
- this.respond( 500, 'text/html', utils.errorPage( 500, 'Internal
Server Error', reason ) );
- },
-
- respond: function( code, type, content ) {
- for ( var i = 0; i < this.requests.length; i++ ) {
- this.requests[i].respond( code, type, content )
- }
- },
-
- sendToWorker: function( worker ) {
- worker.work = this;
- worker.send( { code: 'minify', id: this.id, text: this.text } );
- }
-};
-
-module.exports = Work;
\ No newline at end of file
diff --git a/server/worker.js b/server/worker.js
index 4d83e1b..82288ea 100644
--- a/server/worker.js
+++ b/server/worker.js
@@ -26,6 +26,7 @@
log( 'Minified JS fragment ' + id + ' in ' + time + 'us.' );
process.send( { code: 'minified', id: id, text: text, time:
time } );
} catch ( ex ) {
+ log( "Exception: " + ex.toString() );
process.send( { code: 'exception', id: id, text: ex.toString()
} );
}
}
--
To view, visit https://gerrit.wikimedia.org/r/90770
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I9e574a990f3401d8005aab2b9bd1534f51a8f186
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Minifier
Gerrit-Branch: master
Gerrit-Owner: MaxSem <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits