Răsfoiți Sursa

Merge branch 't/41'

Szymon Kupś 9 ani în urmă
părinte
comite
82d6415755

+ 4 - 3
dev/tasks/build/tasks.js

@@ -15,6 +15,7 @@ const gutil = require( 'gulp-util' );
 const filter = require( 'gulp-filter' );
 const utils = require( './utils' );
 const runSequence = require( 'run-sequence' );
+const tools = require( '../../utils/tools' );
 
 module.exports = ( config ) => {
 	const buildDir = path.join( config.ROOT_DIR, config.BUILD_DIR );
@@ -33,7 +34,7 @@ module.exports = ( config ) => {
 			themes( options = {} ) {
 				let formats = options.formats || args.formats;
 
-				return utils.clean( buildDir, path.join( `@(${ formats.join( '|' ) })`, 'theme' ) );
+				return tools.clean( buildDir, path.join( `@(${ formats.join( '|' ) })`, 'theme' ) );
 			},
 
 			/**
@@ -43,14 +44,14 @@ module.exports = ( config ) => {
 				// TODO: ES6 default function parameters
 				options = options || utils.parseArguments();
 
-				return utils.clean( buildDir, path.join( `@(${ options.formats.join( '|' ) })`, '!(theme)' ) );
+				return tools.clean( buildDir, path.join( `@(${ options.formats.join( '|' ) })`, '!(theme)' ) );
 			},
 
 			/**
 			 * Removes the "./build" directory.
 			 */
 			all() {
-				return utils.clean( buildDir, path.join() );
+				return tools.clean( buildDir, path.join() );
 			}
 		},
 

+ 2 - 4
dev/tasks/build/utils.js

@@ -21,7 +21,6 @@ const minimist = require( 'minimist' );
 const sprite = require( 'gulp-svg-sprite' );
 const pipe = require( 'multipipe' );
 const filter = require( 'gulp-filter' );
-const mainUtils = require( '../utils' );
 
 const utils = {
 	/**
@@ -518,7 +517,7 @@ require( [ 'tests' ], bender.defer(), function( err ) {
 	},
 
 	/**
-	 * Given a stream of .svg files it returns a stream containing JavaScript
+	 * Given a stream of `.svg` files it returns a stream containing JavaScript
 	 * icon sprite file.
 	 *
 	 * @returns {Stream}
@@ -612,5 +611,4 @@ require( [ 'tests' ], bender.defer(), function( err ) {
 	}
 };
 
-// Extend top level utils.
-module.exports = Object.assign( utils, mainUtils );
+module.exports = utils;

+ 4 - 3
dev/tasks/bundle/tasks.js

@@ -16,6 +16,7 @@ const utils = require( './utils' );
 const rollup = require( 'rollup' ).rollup;
 const rollupBabel = require( 'rollup-plugin-babel' );
 const mkdirp = require( 'mkdirp' );
+const tools = require( '../../utils/tools' );
 
 module.exports = ( config ) => {
 	const args = utils.parseArguments();
@@ -29,7 +30,7 @@ module.exports = ( config ) => {
 		 * Removes all files from bundle directory.
 		 */
 		clean() {
-			return utils.clean( bundleDir, '*.*' );
+			return tools.clean( bundleDir, '*.*' );
 		},
 
 		/**
@@ -107,14 +108,14 @@ module.exports = ( config ) => {
 				} )
 				.then( () => {
 					// If everything went well then remove tmp directory.
-					utils.clean( bundleTmpDir, path.join( '' ) );
+					tools.clean( bundleTmpDir, path.join( '' ) );
 				} )
 				.catch( ( err ) => {
 					// If something went wrong then log error.
 					gutil.log( gutil.colors.red( err.stack ) );
 
 					// And remove tmp directory.
-					utils.clean( bundleTmpDir, path.join( '' ) );
+					tools.clean( bundleTmpDir, path.join( '' ) );
 
 					throw new Error( 'Build error.' );
 				} );

+ 1 - 3
dev/tasks/bundle/utils.js

@@ -12,7 +12,6 @@ const gulpRename = require( 'gulp-rename' );
 const gutil = require( 'gulp-util' );
 const prettyBytes = require( 'pretty-bytes' );
 const gzipSize = require( 'gzip-size' );
-const mainUtils = require( '../utils' );
 const minimist = require( 'minimist' );
 
 const utils = {
@@ -236,5 +235,4 @@ export default class ${ data.moduleName } extends ${ creatorName } {
 	}
 };
 
-// Extend top level utils.
-module.exports = Object.assign( utils, mainUtils );
+module.exports = utils;

+ 0 - 9
dev/tasks/dev/tasks.js

@@ -13,19 +13,10 @@ const installTask = require( './tasks/install' );
 const pluginCreateTask = require( './tasks/create-package' );
 const updateTask = require( './tasks/update' );
 const relinkTask = require( './tasks/relink' );
-const log = require( './utils/log' );
-const gutil = require( 'gulp-util' );
 
 module.exports = ( config ) => {
 	const ckeditor5Path = process.cwd();
 	const packageJSON = require( '../../../package.json' );
-
-	// Configure logging.
-	log.configure(
-		( msg ) => gutil.log( msg ),
-		( msg ) => gutil.log( gutil.colors.red( msg ) )
-	);
-
 	const tasks = {
 		updateRepositories() {
 			const options = minimist( process.argv.slice( 2 ), {

+ 3 - 3
dev/tasks/dev/tasks/create-package.js

@@ -6,10 +6,10 @@
 'use strict';
 
 const inquiries = require( '../utils/inquiries' );
-const git = require( '../utils/git' );
-const tools = require( '../utils/tools' );
+const git = require( '../../../utils/git' );
+const tools = require( '../../../utils/tools' );
 const path = require( 'path' );
-const log = require( '../utils/log' );
+const log = require( '../../../utils/log' );
 
 /**
  * 1. Ask for new package name.

+ 3 - 3
dev/tasks/dev/tasks/init.js

@@ -5,8 +5,8 @@
 
 'use strict';
 
-const tools = require( '../utils/tools' );
-const log = require( '../utils/log' );
+const ckeditor5Dirs = require( '../../../utils/ckeditor5-dirs' );
+const log = require( '../../../utils/log' );
 
 /**
  * 1. Get CKEditor5 dependencies from package.json file.
@@ -19,7 +19,7 @@ const log = require( '../utils/log' );
  */
 module.exports = ( installTask, ckeditor5Path, packageJSON, workspaceRoot ) => {
 	// Get all CKEditor dependencies from package.json.
-	const dependencies = tools.getCKEditorDependencies( packageJSON.dependencies );
+	const dependencies = ckeditor5Dirs.getDependencies( packageJSON.dependencies );
 
 	if ( dependencies ) {
 		for ( let dependency in dependencies ) {

+ 3 - 3
dev/tasks/dev/tasks/install.js

@@ -5,10 +5,10 @@
 
 'use strict';
 
-const git = require( '../utils/git' );
-const tools = require( '../utils/tools' );
+const git = require( '../../../utils/git' );
+const tools = require( '../../../utils/tools' );
 const path = require( 'path' );
-const log = require( '../utils/log' );
+const log = require( '../../../utils/log' );
 
 /**
  * This tasks install specified package in development mode. It can be executed by typing:

+ 5 - 4
dev/tasks/dev/tasks/relink.js

@@ -5,9 +5,10 @@
 
 'use strict';
 
-const tools = require( '../utils/tools' );
+const ckeditor5Dirs = require( '../../../utils/ckeditor5-dirs' );
+const tools = require( '../../../utils/tools' );
 const path = require( 'path' );
-const log = require( '../utils/log' );
+const log = require( '../../../utils/log' );
 
 /**
  * 1. Get CKEditor5 dependencies from package.json file.
@@ -22,10 +23,10 @@ module.exports = ( ckeditor5Path, packageJSON, workspaceRoot ) => {
 	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
 
 	// Get all CKEditor dependencies from package.json.
-	const dependencies = tools.getCKEditorDependencies( packageJSON.dependencies );
+	const dependencies = ckeditor5Dirs.getDependencies( packageJSON.dependencies );
 
 	if ( dependencies ) {
-		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
+		const directories = ckeditor5Dirs.getDirectories( workspaceAbsolutePath );
 
 		if ( directories.length ) {
 			for ( let dependency in dependencies ) {

+ 5 - 5
dev/tasks/dev/tasks/status.js

@@ -5,10 +5,10 @@
 
 'use strict';
 
-const tools = require( '../utils/tools' );
-const git = require( '../utils/git' );
+const ckeditor5Dirs = require( '../../../utils/ckeditor5-dirs' );
+const git = require( '../../../utils/git' );
 const path = require( 'path' );
-const log = require( '../utils/log' );
+const log = require( '../../../utils/log' );
 
 /**
  * 1. Get CKEditor5 dependencies from package.json file.
@@ -23,10 +23,10 @@ module.exports = ( ckeditor5Path, packageJSON, workspaceRoot ) => {
 	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
 
 	// Get all CKEditor dependencies from package.json.
-	const dependencies = tools.getCKEditorDependencies( packageJSON.dependencies );
+	const dependencies = ckeditor5Dirs.getDependencies( packageJSON.dependencies );
 
 	if ( dependencies ) {
-		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
+		const directories = ckeditor5Dirs.getDirectories( workspaceAbsolutePath );
 
 		if ( directories.length ) {
 			for ( let dependency in dependencies ) {

+ 7 - 6
dev/tasks/dev/tasks/update.js

@@ -5,10 +5,11 @@
 
 'use strict';
 
-const tools = require( '../utils/tools' );
-const git = require( '../utils/git' );
+const tools = require( '../../../utils/tools' );
+const ckeditor5Dirs = require( '../../../utils/ckeditor5-dirs' );
+const git = require( '../../../utils/git' );
 const path = require( 'path' );
-const log = require( '../utils/log' );
+const log = require( '../../../utils/log' );
 
 /**
  * 1. Fetch all branches from each origin in main CKEditor 5 repository.
@@ -38,10 +39,10 @@ module.exports = ( installTask, ckeditor5Path, packageJSON, workspaceRoot, runNp
 	git.fetchAll( ckeditor5Path );
 
 	// Get all CKEditor dependencies from package.json.
-	const dependencies = tools.getCKEditorDependencies( packageJSON.dependencies );
+	const dependencies = ckeditor5Dirs.getDependencies( packageJSON.dependencies );
 
 	if ( dependencies ) {
-		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
+		const directories = ckeditor5Dirs.getDirectories( workspaceAbsolutePath );
 
 		for ( let dependency in dependencies ) {
 			const repositoryURL = dependencies[ dependency ];
@@ -86,7 +87,7 @@ module.exports = ( installTask, ckeditor5Path, packageJSON, workspaceRoot, runNp
 
 	// Remove symlinks not used in this configuration.
 	const nodeModulesPath = path.join( ckeditor5Path, 'node_modules' );
-	const symlinks = tools.getCKE5Symlinks( nodeModulesPath );
+	const symlinks = ckeditor5Dirs.getSymlinks( nodeModulesPath );
 	symlinks
 		.filter( dir => typeof dependencies[ dir ] == 'undefined' )
 		.forEach( dir => {

+ 38 - 0
dev/tasks/exec/functions/bump-year.js

@@ -0,0 +1,38 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const gulp = require( 'gulp' );
+const path = require( 'path' );
+const replace = require( 'gulp-replace' );
+const filterGitignore = require( '../utils/filtergitignore' );
+
+// Change this to correct year.
+const year = '2017';
+
+/**
+ * Replaces license date in source files with new date.
+ *
+ * Example (remember to change the year harcoded in the module):
+ *
+ *		gulp exec --task bump-year
+ *
+ * @param {String} workdir
+ * @returns {Stream}
+ */
+module.exports = function executeBumpYear( workdir ) {
+	const licenseRegexp = /(@license Copyright \(c\) 2003-)[0-9]{4}(, CKSource - Frederico Knabben\.)/g;
+	const glob = path.join( workdir, '**/*' );
+
+	return gulp.src( glob )
+		.pipe( filterGitignore() )
+		.pipe( replace(
+			licenseRegexp,
+			`$1${ year }$2`,
+			{ skipBinary: true }
+		) )
+		.pipe( gulp.dest( workdir ) );
+};

+ 28 - 0
dev/tasks/exec/functions/git-commit.js

@@ -0,0 +1,28 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const git = require( '../../../utils/git' );
+
+/**
+ * Adds only modified files to git repository and commits them with provided message.
+ *
+ * Example:
+ *
+ *		gulp exec --task git-commit --message "Commit message."
+ *
+ * @param {String} workdir
+ * @param {Object} params
+ */
+module.exports = function executeGitCommit( workdir, params ) {
+	const message = params.message;
+
+	if ( !message ) {
+		throw new Error( 'You must provide commit message with parameter: --message' );
+	}
+
+	git.commit( message, workdir );
+};

+ 21 - 0
dev/tasks/exec/functions/git-push.js

@@ -0,0 +1,21 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const git = require( '../../../utils/git' );
+
+/**
+ * Pushes changes of current branch in repository to default origin.
+ *
+ * Example:
+ *
+ *		gulp exec --task git-push
+ *
+ * @param {String} workdir
+ */
+module.exports = function executeGitPush( workdir ) {
+	git.push( workdir );
+};

+ 102 - 0
dev/tasks/exec/tasks.js

@@ -0,0 +1,102 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const gulp = require( 'gulp' );
+const minimist = require( 'minimist' );
+const path = require( 'path' );
+const merge = require( 'merge-stream' );
+const log = require( '../../utils/log' );
+const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
+
+/**
+ * Run task over `ckeditor5-*` repositories.
+ *
+ * Example:
+ *
+ *		gulp exec --task task-name
+ *
+ * Example of running task just for one repository:
+ *
+ *		gulp exec --task task-name --repository ckeditor5-utils
+ *
+ * @param {Object} config Task runner configuration.
+ * @returns {Stream} Stream with processed files.
+ */
+module.exports = ( config ) => {
+	const ckeditor5Path = process.cwd();
+	const packageJSON = require( '../../../package.json' );
+
+	const tasks = {
+		execOnRepositories() {
+			// Omit `gulp exec` part of arguments
+			const params = minimist( process.argv.slice( 3 ), {
+				stopEarly: false,
+			} );
+			let task;
+
+			try {
+				if ( params.task ) {
+					task = require( `./functions/${ params.task }` );
+				} else {
+					throw new Error( 'Missing task parameter: --task task-name' );
+				}
+			} catch ( err ) {
+				log.err( err );
+
+				return;
+			}
+
+			return execute( task, ckeditor5Path, packageJSON, config.WORKSPACE_DIR, params );
+		},
+
+		register() {
+			gulp.task( 'exec', tasks.execOnRepositories );
+		}
+	};
+
+	return tasks;
+};
+
+/**
+ * Execute given task with provided options and command-line parameters.
+ *
+ * @param {Function} execTask Task to use on each dependency.
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {Object} packageJSON Parsed `package.json` file from CKEditor 5 repository.
+ * @param {String} workspaceRoot Relative path to workspace root.
+ * @param {Object} params Parameters provided to the task via command-line.
+ * @returns {Stream} Merged stream of processed files.
+ */
+function execute( execTask, ckeditor5Path, packageJSON, workspaceRoot, params ) {
+	const workspacePath = path.join( ckeditor5Path, workspaceRoot );
+	const mergedStream = merge();
+	const specificRepository = params.repository;
+
+	let devDirectories = ckeditor5Dirs.getDevDirectories( workspacePath, packageJSON, ckeditor5Path );
+
+	if ( specificRepository ) {
+		devDirectories = devDirectories.filter( ( dir ) => {
+			return dir.repositoryURL === `ckeditor/${ specificRepository }`;
+		} );
+	}
+
+	for ( const dir of devDirectories ) {
+		try {
+			log.out( `Executing task on ${ dir.repositoryURL }...` );
+
+			const result = execTask( dir.repositoryPath, params );
+
+			if ( result ) {
+				mergedStream.add( result );
+			}
+		} catch ( err ) {
+			log.err( err );
+		}
+	}
+
+	return mergedStream;
+}

+ 28 - 0
dev/tasks/exec/utils/filtergitignore.js

@@ -0,0 +1,28 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const fs = require( 'fs' );
+const filter = require( 'gulp-filter' );
+const parseGitignore = require( 'parse-gitignore' );
+const PassThrough = require( 'stream' ).PassThrough;
+
+module.exports = function filterGitignore() {
+	const fileName = '.gitignore';
+
+	if ( !fs.existsSync( fileName ) ) {
+		return new PassThrough( { objectMode: true } );
+	}
+
+	const gitignoreGlob =
+		parseGitignore( fileName )
+			// Invert '!foo' -> 'foo' and 'foo' -> '!foo'.
+			.map( pattern => pattern.startsWith( '!' ) ? pattern.slice( 1 ) : '!' + pattern );
+
+	gitignoreGlob.unshift( '**/*' );
+
+	return filter( gitignoreGlob );
+};

+ 1 - 1
dev/tasks/test/tasks.js

@@ -10,7 +10,7 @@ const chai = require( 'chai' );
 const filterBy = require( 'gulp-filter-by' );
 const filter = require( 'gulp-filter' );
 const sinon = require( 'sinon' );
-const devTools = require( '../dev/utils/tools' );
+const devTools = require( '../../utils/tools' );
 const semver = require( 'semver' );
 const buildUtils = require( '../build/utils' );
 const benderConfig = require( '../../../bender' );

+ 0 - 30
dev/tasks/utils.js

@@ -1,30 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-const path = require( 'path' );
-const del = require( 'del' );
-const gutil = require( 'gulp-util' );
-
-const utils = {
-	/**
-	 * Removes files and directories specified by `glob` starting from `rootDir`
-	 * and gently informs about deletion.
-	 *
-	 * @param {String} rootDir The path to the root directory (i.e. "dist/").
-	 * @param {String} glob Glob specifying what to clean.
-	 * @returns {Promise}
-	 */
-	clean( rootDir, glob ) {
-		return del( path.join( rootDir, glob ) ).then( paths => {
-			paths.forEach( p => {
-				gutil.log( `Deleted file '${ gutil.colors.cyan( p ) }'.` );
-			} );
-		} );
-	}
-};
-
-module.exports = utils;

+ 0 - 5
dev/tests/build/utils.js

@@ -16,7 +16,6 @@ const path = require( 'path' );
 const stream = require( 'stream' );
 const Vinyl = require( 'vinyl' );
 const through = require( 'through2' );
-const mainUtils = require( '../../tasks/utils' );
 
 describe( 'build-utils', () => {
 	const utils = require( '../../tasks/build/utils' );
@@ -30,10 +29,6 @@ describe( 'build-utils', () => {
 		sandbox.restore();
 	} );
 
-	it( 'should be extended by top level utils', () => {
-		expect( utils.clean ).to.be.equal( mainUtils.clean );
-	} );
-
 	describe( 'noop', () => {
 		it( 'should return PassTrough stream', () => {
 			const PassThrough = stream.PassThrough;

+ 0 - 5
dev/tests/bundle/utils.js

@@ -12,7 +12,6 @@ const expect = chai.expect;
 const sinon = require( 'sinon' );
 const path = require( 'path' );
 const fs = require( 'fs' );
-const mainUtils = require( '../../tasks/utils' );
 const gzipSize = require( 'gzip-size' );
 
 describe( 'bundle-utils', () => {
@@ -27,10 +26,6 @@ describe( 'bundle-utils', () => {
 		sandbox.restore();
 	} );
 
-	it( 'should be extended by top level utils', () => {
-		expect( utils.clean ).to.equal( mainUtils.clean );
-	} );
-
 	describe( 'getModuleFullPath', () => {
 		it( 'should return full path when passed path not relative', () => {
 			expect( utils.getModuleFullPath( 'editor-classic/classic' ) ).to.equal( './build/esnext/ckeditor5/editor-classic/classic.js' );

+ 2 - 2
dev/tests/dev/create-package.js

@@ -10,9 +10,9 @@
 const chai = require( 'chai' );
 const sinon = require( 'sinon' );
 const expect = chai.expect;
-const tools = require( '../../tasks/dev/utils/tools' );
+const tools = require( '../../utils/tools' );
 const inquiries = require( '../../tasks/dev/utils/inquiries' );
-const git = require( '../../tasks/dev/utils/git' );
+const git = require( '../../utils/git' );
 const path = require( 'path' );
 
 describe( 'dev-create-package', () => {

+ 3 - 3
dev/tests/dev/init.js

@@ -8,7 +8,7 @@
 'use strict';
 
 const sinon = require( 'sinon' );
-const tools = require( '../../tasks/dev/utils/tools' );
+const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
 
 describe( 'dev-init', () => {
 	const initTask = require( '../../tasks/dev/tasks/init' );
@@ -16,7 +16,7 @@ describe( 'dev-init', () => {
 	const workspaceRoot = '..';
 
 	it( 'should get all ckedtior5- dependencies and execute dev-install on them', () => {
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
 		const installSpy = sinon.spy();
 		const JSON = {
 			dependencies: {
@@ -39,7 +39,7 @@ describe( 'dev-init', () => {
 	} );
 
 	it( 'should not call dev-install if no ckedtior5- dependencies', () => {
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
 		const installSpy = sinon.spy();
 		const JSON = {
 			dependencies: {}

+ 2 - 2
dev/tests/dev/install.js

@@ -9,8 +9,8 @@
 
 const chai = require( 'chai' );
 const sinon = require( 'sinon' );
-const git = require( '../../tasks/dev/utils/git' );
-const tools = require( '../../tasks/dev/utils/tools' );
+const git = require( '../../utils/git' );
+const tools = require( '../../utils/tools' );
 const installTask = require( '../../tasks/dev/tasks/install' );
 const expect = chai.expect;
 const path = require( 'path' );

+ 11 - 10
dev/tests/dev/relink.js

@@ -8,7 +8,8 @@
 'use strict';
 
 const sinon = require( 'sinon' );
-const tools = require( '../../tasks/dev/utils/tools' );
+const tools = require( '../../utils/tools' );
+const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
 const path = require( 'path' );
 
 describe( 'dev-relink', () => {
@@ -20,8 +21,8 @@ describe( 'dev-relink', () => {
 
 	it( 'should link dev repositories', () => {
 		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
+		const getDirectoriesStub = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( dirs );
 		const linkStub = sinon.stub( tools, 'linkDirectories' );
 		const json = {
 			dependencies: {
@@ -43,8 +44,8 @@ describe( 'dev-relink', () => {
 	} );
 
 	it( 'should not link when no dependencies are found', () => {
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
+		const getDirectoriesStub = sinon.stub( ckeditor5Dirs, 'getDirectories' );
 		const linkStub = sinon.stub( tools, 'linkDirectories' );
 		const json = {
 			dependencies: {
@@ -62,8 +63,8 @@ describe( 'dev-relink', () => {
 	} );
 
 	it( 'should not link when no plugins in dev mode', () => {
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' ).returns( [] );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
+		const getDirectoriesStub = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( [] );
 		const linkStub = sinon.stub( tools, 'linkDirectories' );
 		const json = {
 			dependencies: {
@@ -83,8 +84,8 @@ describe( 'dev-relink', () => {
 
 	it( 'should write error message when linking is unsuccessful', () => {
 		const dirs = [ 'ckeditor5-core' ];
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
+		const getDirectoriesStub = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( dirs );
 		const error = new Error( 'Error message.' );
 		const linkStub = sinon.stub( tools, 'linkDirectories' ).throws( error );
 		const json = {
@@ -95,7 +96,7 @@ describe( 'dev-relink', () => {
 			}
 		};
 		const writeErrorSpy = sinon.spy();
-		const log = require( '../../tasks/dev/utils/log' );
+		const log = require( '../../utils/log' );
 		log.configure( () => {}, writeErrorSpy );
 
 		task( ckeditor5Path, json, workspaceRoot );

+ 11 - 11
dev/tests/dev/status.js

@@ -8,8 +8,8 @@
 'use strict';
 
 const sinon = require( 'sinon' );
-const tools = require( '../../tasks/dev/utils/tools' );
-const git = require( '../../tasks/dev/utils/git' );
+const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
+const git = require( '../../utils/git' );
 const path = require( 'path' );
 const chai = require( 'chai' );
 const expect = chai.expect;
@@ -23,8 +23,8 @@ describe( 'dev-status', () => {
 
 	it( 'should show status of dev repositories', () => {
 		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
+		const getDirectoriesStub = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( dirs );
 		const statusStub = sinon.stub( git, 'getStatus' );
 		const json = {
 			dependencies: {
@@ -46,8 +46,8 @@ describe( 'dev-status', () => {
 	} );
 
 	it( 'should not get status when no dependencies are found', () => {
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
+		const getDirectoriesStub = sinon.stub( ckeditor5Dirs, 'getDirectories' );
 		const statusStub = sinon.stub( git, 'getStatus' );
 		const json = {
 			dependencies: {
@@ -65,8 +65,8 @@ describe( 'dev-status', () => {
 	} );
 
 	it( 'should not get status when no plugins in dev mode', () => {
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' ).returns( [] );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
+		const getDirectoriesStub = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( [] );
 		const statusStub = sinon.stub( git, 'getStatus' );
 		const json = {
 			dependencies: {
@@ -86,8 +86,8 @@ describe( 'dev-status', () => {
 
 	it( 'should write error message when getStatus is unsuccessful', () => {
 		const dirs = [ 'ckeditor5-core' ];
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
+		const getDependenciesSpy = sinon.spy( ckeditor5Dirs, 'getDependencies' );
+		const getDirectoriesStub = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( dirs );
 		const error = new Error( 'Error message.' );
 		const statusStub = sinon.stub( git, 'getStatus' ).throws( error );
 		const json = {
@@ -98,7 +98,7 @@ describe( 'dev-status', () => {
 			}
 		};
 		const writeErrorSpy = sinon.spy();
-		const log = require( '../../tasks/dev/utils/log' );
+		const log = require( '../../utils/log' );
 		log.configure( () => {}, writeErrorSpy );
 
 		statusTask( ckeditor5Path, json, workspaceRoot );

+ 21 - 20
dev/tests/dev/update.js

@@ -8,8 +8,9 @@
 'use strict';
 
 const sinon = require( 'sinon' );
-const tools = require( '../../tasks/dev/utils/tools' );
-const git = require( '../../tasks/dev/utils/git' );
+const tools = require( '../../utils/tools' );
+const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
+const git = require( '../../utils/git' );
 const path = require( 'path' );
 const chai = require( 'chai' );
 const expect = chai.expect;
@@ -23,7 +24,7 @@ describe( 'dev-update', () => {
 	const spies = {};
 
 	beforeEach( () => {
-		spies.getDependencies = sinon.spy( tools, 'getCKEditorDependencies' );
+		spies.getDependencies = sinon.spy( ckeditor5Dirs, 'getDependencies' );
 		spies.checkout = sinon.stub( git, 'checkout' );
 		spies.pull = sinon.stub( git, 'pull' );
 		spies.fetchAll = sinon.stub( git, 'fetchAll' );
@@ -39,8 +40,8 @@ describe( 'dev-update', () => {
 	it( 'should update dev repositories', () => {
 		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
 		const installTask = sinon.spy();
-		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
-		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [] );
+		spies.getDirectories = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( dirs );
+		spies.getSymlinks = sinon.stub( ckeditor5Dirs, 'getSymlinks' ).returns( [] );
 
 		const json = {
 			dependencies: {
@@ -71,7 +72,7 @@ describe( 'dev-update', () => {
 		sinon.assert.calledWithExactly( spies.npmUpdate.secondCall, path.join( workspaceAbsolutePath, dirs[ 1 ] ) );
 		sinon.assert.calledWithExactly( spies.npmUpdate.thirdCall, ckeditor5Path );
 
-		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.calledOnce( spies.getSymlinks );
 		sinon.assert.notCalled( spies.removeSymlink );
 		sinon.assert.notCalled( installTask );
 	} );
@@ -79,8 +80,8 @@ describe( 'dev-update', () => {
 	it( 'should install missing dependencies', () => {
 		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
 		const installTask = sinon.spy();
-		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
-		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [] );
+		spies.getDirectories = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( dirs );
+		spies.getSymlinks = sinon.stub( ckeditor5Dirs, 'getSymlinks' ).returns( [] );
 
 		const json = {
 			dependencies: {
@@ -115,15 +116,15 @@ describe( 'dev-update', () => {
 		sinon.assert.calledOnce( installTask );
 		sinon.assert.calledWithExactly( installTask, ckeditor5Path, workspaceRoot, 'ckeditor/ckeditor5-ui' );
 
-		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.calledOnce( spies.getSymlinks );
 		sinon.assert.notCalled( spies.removeSymlink );
 	} );
 
 	it( 'should remove symlinks that are not needed', () => {
 		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
 		const installTask = sinon.spy();
-		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
-		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [ 'ckeditor5-unused' ] );
+		spies.getDirectories = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( dirs );
+		spies.getSymlinks = sinon.stub( ckeditor5Dirs, 'getSymlinks' ).returns( [ 'ckeditor5-unused' ] );
 
 		const json = {
 			dependencies: {
@@ -154,7 +155,7 @@ describe( 'dev-update', () => {
 		sinon.assert.calledWithExactly( spies.npmUpdate.secondCall, path.join( workspaceAbsolutePath, dirs[ 1 ] ) );
 		sinon.assert.calledWithExactly( spies.npmUpdate.thirdCall, ckeditor5Path );
 
-		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.calledOnce( spies.getSymlinks );
 		sinon.assert.notCalled( installTask );
 
 		sinon.assert.calledOnce( spies.removeSymlink );
@@ -162,13 +163,13 @@ describe( 'dev-update', () => {
 	} );
 
 	it( 'should catch linking errors', () => {
-		const log = require( '../../tasks/dev/utils/log' );
+		const log = require( '../../utils/log' );
 		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
 		const installTask = sinon.spy();
 		const outSpy = sinon.spy();
 		const errSpy = sinon.spy();
-		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
-		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [ 'ckeditor5-unused' ] );
+		spies.getDirectories = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( dirs );
+		spies.getSymlinks = sinon.stub( ckeditor5Dirs, 'getSymlinks' ).returns( [ 'ckeditor5-unused' ] );
 		spies.linkDirectories.throws();
 
 		log.configure( outSpy, errSpy );
@@ -199,7 +200,7 @@ describe( 'dev-update', () => {
 
 		sinon.assert.notCalled( spies.npmUpdate );
 
-		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.calledOnce( spies.getSymlinks );
 		sinon.assert.notCalled( installTask );
 
 		sinon.assert.calledOnce( spies.removeSymlink );
@@ -209,11 +210,11 @@ describe( 'dev-update', () => {
 
 	it( 'should skip updating if no dependencies found and fetch only main repository', () => {
 		spies.getDependencies.restore();
-		spies.getDependencies = sinon.stub( tools, 'getCKEditorDependencies' ).returns( null );
+		spies.getDependencies = sinon.stub( ckeditor5Dirs, 'getDependencies' ).returns( null );
 		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
 		const installTask = sinon.spy();
-		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
-		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [] );
+		spies.getDirectories = sinon.stub( ckeditor5Dirs, 'getDirectories' ).returns( dirs );
+		spies.getSymlinks = sinon.stub( ckeditor5Dirs, 'getSymlinks' ).returns( [] );
 
 		const json = {
 			dependencies: {
@@ -233,7 +234,7 @@ describe( 'dev-update', () => {
 
 		sinon.assert.notCalled( spies.npmUpdate );
 
-		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.calledOnce( spies.getSymlinks );
 		sinon.assert.notCalled( installTask );
 
 		sinon.assert.notCalled( spies.removeSymlink );

+ 160 - 0
dev/tests/exec/tasks.js

@@ -0,0 +1,160 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global describe, it, beforeEach, afterEach */
+
+'use strict';
+
+const mockery = require( 'mockery' );
+const sinon = require( 'sinon' );
+const chai = require( 'chai' );
+const expect = chai.expect;
+
+describe( 'exec-tasks', () => {
+	let sandbox;
+	const config = {
+		WORKSPACE_DIR: '/path/exec/'
+	};
+	const getDevDirectoriesResult = [
+		{
+			repositoryPath: '/path/1',
+			repositoryURL: 'ckeditor/test1'
+		},
+		{
+			repositoryPath: '/path/2',
+			repositoryURL: 'ckeditor/test2'
+		}
+	];
+
+	beforeEach( () => {
+		mockery.enable( {
+			useCleanCache: true,
+			warnOnReplace: false,
+			warnOnUnregistered: false
+		} );
+		sandbox = sinon.sandbox.create();
+	} );
+
+	afterEach( () => {
+		mockery.disable();
+		sandbox.restore();
+	} );
+
+	describe( 'execOnRepositories', () => {
+		it( 'should throw error when there is no specified task', () => {
+			const errorMessage = 'Missing task parameter: --task task-name';
+			const log = require( '../../utils/log' );
+			const logErrSpy = sandbox.stub( log, 'err' );
+
+			mockery.registerMock( 'minimist', () => {
+				return { };
+			} );
+			const tasks = require( '../../tasks/exec/tasks' )( config );
+
+			tasks.execOnRepositories();
+
+			sinon.assert.calledOnce( logErrSpy );
+			expect( logErrSpy.firstCall.args[ 0 ] ).to.be.an( 'error' );
+			expect( logErrSpy.firstCall.args[ 0 ].message ).to.equal( errorMessage );
+		} );
+
+		it( 'should throw error when task cannot be found', () => {
+			const log = require( '../../utils/log' );
+			const logErrSpy = sandbox.stub( log, 'err' );
+
+			mockery.registerMock( 'minimist', () => {
+				return { task: 'task-to-run' };
+			} );
+			const tasks = require( '../../tasks/exec/tasks' )( config );
+
+			tasks.execOnRepositories();
+
+			sinon.assert.calledOnce( logErrSpy );
+			expect( logErrSpy.firstCall.args[ 0 ] ).to.be.an( 'error' );
+			expect( logErrSpy.firstCall.args[ 0 ].code ).to.equal( 'MODULE_NOT_FOUND' );
+		} );
+
+		it( 'should load task module', () => {
+			const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
+			const log = require( '../../utils/log' );
+			const logErrSpy = sandbox.stub( log, 'err' );
+
+			sandbox.stub( ckeditor5Dirs, 'getDevDirectories' ).returns( [] );
+			mockery.registerMock( 'minimist', () => {
+				return { task: 'task-to-run' };
+			} );
+			mockery.registerMock( './functions/task-to-run', () => {} );
+			const tasks = require( '../../tasks/exec/tasks' )( config );
+
+			tasks.execOnRepositories();
+
+			sinon.assert.notCalled( logErrSpy );
+		} );
+
+		it( 'should log error when task is throwing exceptions', () => {
+			const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
+			const taskStub = sinon.stub();
+			const log = require( '../../utils/log' );
+			const logErrSpy = sandbox.stub( log, 'err' );
+
+			taskStub.onSecondCall().throws();
+
+			mockery.registerMock( 'minimist', () => {
+				return { task: 'task-to-run' };
+			} );
+			sandbox.stub( ckeditor5Dirs, 'getDevDirectories' ).returns( getDevDirectoriesResult );
+			mockery.registerMock( './functions/task-to-run', taskStub );
+			const tasks = require( '../../tasks/exec/tasks' )( config );
+
+			tasks.execOnRepositories();
+
+			sinon.assert.calledOnce( logErrSpy );
+			expect( logErrSpy.firstCall.args[ 0 ] ).to.be.an( 'error' );
+			sinon.assert.calledTwice( taskStub );
+			sinon.assert.calledWith( taskStub, '/path/1', { task: 'task-to-run' } );
+			sinon.assert.calledWith( taskStub, '/path/2', { task: 'task-to-run' } );
+		} );
+
+		it( 'should execute task over directories', () => {
+			const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
+			const taskStub = sinon.stub();
+
+			mockery.registerMock( 'minimist', () => {
+				return { task: 'task-to-run' };
+			} );
+			sandbox.stub( ckeditor5Dirs, 'getDevDirectories' ).returns( getDevDirectoriesResult );
+			mockery.registerMock( './functions/task-to-run', taskStub );
+			const tasks = require( '../../tasks/exec/tasks' )( config );
+
+			tasks.execOnRepositories();
+
+			sinon.assert.calledTwice( taskStub );
+			sinon.assert.calledWith( taskStub, '/path/1', { task: 'task-to-run' } );
+			sinon.assert.calledWith( taskStub, '/path/2', { task: 'task-to-run' } );
+		} );
+
+		it( 'should execute task over specific directory', () => {
+			const Stream = require( 'stream' );
+			const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
+			const taskStub = sinon.stub().returns( new Stream() );
+
+			mockery.registerMock( 'minimist', () => {
+				return {
+					task: 'task-to-run',
+					repository: 'test1'
+				};
+			} );
+			sandbox.stub( ckeditor5Dirs, 'getDevDirectories' ).returns( getDevDirectoriesResult );
+			mockery.registerMock( './functions/task-to-run', taskStub );
+			const tasks = require( '../../tasks/exec/tasks' )( config );
+
+			tasks.execOnRepositories();
+
+			sinon.assert.calledOnce( taskStub );
+			sinon.assert.calledWith( taskStub, '/path/1', { task: 'task-to-run', repository: 'test1' } );
+			sinon.assert.neverCalledWith( taskStub, '/path/2', { task: 'task-to-run', repository: 'test1' } );
+		} );
+	} );
+} );

+ 1 - 1
dev/tests/test/tasks.js

@@ -9,7 +9,7 @@
 
 const tasks = require( '../../tasks/test/tasks' )();
 const buildUtils = require( '../../tasks/build/utils' );
-const devTools = require( '../../tasks/dev/utils/tools' );
+const devTools = require( '../../utils/tools' );
 const Vinyl = require( 'vinyl' );
 
 describe( 'test-node', () => {

+ 146 - 0
dev/tests/utils/ckeditor5-dirs.js

@@ -0,0 +1,146 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global describe, it, beforeEach, afterEach */
+
+'use strict';
+
+const chai = require( 'chai' );
+const sinon = require( 'sinon' );
+const expect = chai.expect;
+const path = require( 'path' );
+const ckeditor5Dirs = require( '../../utils/ckeditor5-dirs' );
+const tools = require( '../../utils/tools' );
+const git = require( '../../utils/git' );
+let sandbox;
+
+describe( 'utils', () => {
+	beforeEach( () => {
+		sandbox = sinon.sandbox.create();
+	} );
+
+	afterEach( () => {
+		sandbox.restore();
+	} );
+
+	describe( 'ckeditor5dirs', () => {
+		describe( 'getDependencies', () => {
+			it( 'should be defined', () => expect( ckeditor5Dirs.getDependencies ).to.be.a( 'function' ) );
+
+			it( 'should return null if no CKEditor5 repository is found', () => {
+				const packageJSONDependencies = {
+					'plugin1': '',
+					'plugin2': '',
+					'plugin3': ''
+				};
+				expect( ckeditor5Dirs.getDependencies( packageJSONDependencies ) ).to.equal( null );
+				expect( ckeditor5Dirs.getDependencies() ).to.equal( null );
+			} );
+
+			it( 'should return only ckeditor5- dependencies', () => {
+				const packageJSONDependencies = {
+					'plugin1': '',
+					'ckeditor5-plugin-image': 'ckeditor/ckeditor5-plugin-image',
+					'plugin2': '',
+					'ckeditor5-core': 'ckeditor/ckeditor5-core'
+				};
+				const ckeditorDependencies = ckeditor5Dirs.getDependencies( packageJSONDependencies );
+
+				expect( ckeditorDependencies ).to.be.an( 'object' );
+				expect( ckeditorDependencies.plugin1 ).to.be.a( 'undefined' );
+				expect( ckeditorDependencies.plugin2 ).to.be.a( 'undefined' );
+				expect( ckeditorDependencies[ 'ckeditor5-plugin-image' ] ).to.be.a( 'string' );
+				expect( ckeditorDependencies[ 'ckeditor5-core' ] ).to.be.a( 'string' );
+			} );
+		} );
+
+		describe( 'getDirectories', () => {
+			it( 'should be defined', () => expect( ckeditor5Dirs.getDirectories ).to.be.a( 'function' ) );
+
+			it( 'should return only ckeditor5 directories', () => {
+				const workspacePath = '/workspace/path';
+				const sourceDirectories = [ 'tools', 'ckeditor5', 'ckeditor5-core', '.bin', 'ckeditor5-plugin-image' ];
+				sandbox.stub( tools, 'getDirectories', () => sourceDirectories );
+				const directories = ckeditor5Dirs.getDirectories( workspacePath );
+
+				expect( directories.length ).equal( 2 );
+				expect( directories[ 0 ] ).equal( 'ckeditor5-core' );
+				expect( directories[ 1 ] ).equal( 'ckeditor5-plugin-image' );
+			} );
+		} );
+
+		describe( 'getSymlinks', () => {
+			it( 'should return CKE5 symlinks from provided path', () => {
+				const fs = require( 'fs' );
+				const path = 'path/to/dir';
+				sandbox.stub( fs, 'readdirSync' ).returns( [ 'ckeditor5-core', 'ckeditor5-image', 'other-dependency' ] );
+				sandbox.stub( tools, 'isSymlink' ).returns( true );
+
+				const symlinks = ckeditor5Dirs.getSymlinks( path );
+				expect( symlinks.length ).to.equal( 2 );
+				expect( symlinks[ 0 ] ).to.equal( 'ckeditor5-core' );
+				expect( symlinks[ 1 ] ).to.equal( 'ckeditor5-image' );
+			} );
+		} );
+
+		describe( 'getDevDirectories', () => {
+			const packageJSONDependencies = {};
+			const workspacePath = '/workspace/path';
+			const ckeditor5Path = path.join( workspacePath, 'ckeditor5' );
+			const dependencies = {
+				'ckeditor5-plugin-image': 'ckeditor/ckeditor5-plugin-image',
+				'ckeditor5-core': 'ckeditor/ckeditor5-core'
+			};
+			const sourceDirectories = [ 'tools', 'ckeditor5', 'ckeditor5-core', '.bin', 'ckeditor5-plugin-image' ];
+			const repositoryInfo = { name: 'ckeditor5-core' };
+
+			it( 'should be defined', () => expect( ckeditor5Dirs.getDevDirectories ).to.be.a( 'function' ) );
+
+			it( 'should return empty array if no dev directories were found - because of missing ckeditor5-* repos', () => {
+				const wrongRepositoryInfo = { name: 'plugins/plugin' };
+
+				sandbox.stub( ckeditor5Dirs, 'getDirectories', () => sourceDirectories );
+				sandbox.stub( ckeditor5Dirs, 'getDependencies', () => dependencies );
+				sandbox.stub( git, 'parseRepositoryUrl' ).returns( wrongRepositoryInfo );
+
+				const directories = ckeditor5Dirs.getDevDirectories( workspacePath, packageJSONDependencies, ckeditor5Path );
+
+				expect( directories ).to.be.a( 'array' );
+				expect( directories.length ).to.equal( 0 );
+			} );
+
+			it( 'should return empty array if no dev directories were found - because of missing ckeditor5-* dirs', () => {
+				const wrongDirectories = [ 'tools', 'ckeditor5', '.bin' ];
+
+				sandbox.stub( ckeditor5Dirs, 'getDirectories', () => wrongDirectories );
+				sandbox.stub( ckeditor5Dirs, 'getDependencies', () => dependencies );
+				sandbox.stub( git, 'parseRepositoryUrl' ).returns( repositoryInfo );
+
+				const directories = ckeditor5Dirs.getDevDirectories( workspacePath, packageJSONDependencies, ckeditor5Path );
+
+				expect( directories ).to.be.a( 'array' );
+				expect( directories.length ).to.equal( 0 );
+			} );
+
+			it( 'should return only ckeditor5 directories in development mode', () => {
+				sandbox.stub( ckeditor5Dirs, 'getDirectories', () => sourceDirectories );
+				sandbox.stub( ckeditor5Dirs, 'getDependencies', () => dependencies );
+				sandbox.stub( git, 'parseRepositoryUrl' ).returns( repositoryInfo );
+
+				const directories = ckeditor5Dirs.getDevDirectories( workspacePath, packageJSONDependencies, ckeditor5Path );
+
+				expect( directories.length ).equal( 2 );
+				expect( directories[ 0 ] ).eql( {
+						repositoryURL: 'ckeditor/ckeditor5-plugin-image',
+						repositoryPath: '/workspace/path/ckeditor5/node_modules/ckeditor5-plugin-image'
+					} );
+				expect( directories[ 1 ] ).eql( {
+						repositoryURL: 'ckeditor/ckeditor5-core',
+						repositoryPath: '/workspace/path/ckeditor5/node_modules/ckeditor5-core'
+					} );
+			} );
+		} );
+	} );
+} );

+ 2 - 2
dev/tests/dev/git.js → dev/tests/utils/git.js

@@ -8,10 +8,10 @@
 'use strict';
 
 let sandbox;
-const git = require( '../../tasks/dev/utils/git' );
+const git = require( '../../utils/git' );
 const chai = require( 'chai' );
 const sinon = require( 'sinon' );
-const tools = require( '../../tasks/dev/utils/tools' );
+const tools = require( '../../utils/tools' );
 const expect = chai.expect;
 
 describe( 'utils', () => {

+ 1 - 1
dev/tests/dev/log.js → dev/tests/utils/log.js

@@ -8,7 +8,7 @@
 'use strict';
 
 const sinon = require( 'sinon' );
-const log = require( '../../tasks/dev/utils/log' );
+const log = require( '../../utils/log' );
 
 describe( 'log', () => {
 	it( 'should log using configured functions', () => {

+ 3 - 62
dev/tests/dev/tools.js → dev/tests/utils/tools.js

@@ -10,7 +10,7 @@
 const chai = require( 'chai' );
 const sinon = require( 'sinon' );
 const expect = chai.expect;
-const tools = require( '../../tasks/dev/utils/tools' );
+const tools = require( '../../utils/tools' );
 const path = require( 'path' );
 const fs = require( 'fs' );
 let sandbox;
@@ -62,7 +62,7 @@ describe( 'utils', () => {
 			it( 'should not log when in silent mode', () => {
 				const sh = require( 'shelljs' );
 				const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0, stdout: 'out', stderr: 'err' } );
-				const log = require( '../../tasks/dev/utils/log' );
+				const log = require( '../../utils/log' );
 				const outFn = sandbox.spy();
 				const errFn = sandbox.spy();
 				log.configure( outFn, errFn );
@@ -77,7 +77,7 @@ describe( 'utils', () => {
 			it( 'should not log if no output from executed command', () => {
 				const sh = require( 'shelljs' );
 				const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0, stdout: '', stderr: '' } );
-				const log = require( '../../tasks/dev/utils/log' );
+				const log = require( '../../utils/log' );
 				const outFn = sandbox.spy();
 				const errFn = sandbox.spy();
 				log.configure( outFn, errFn );
@@ -147,36 +147,6 @@ describe( 'utils', () => {
 			} );
 		} );
 
-		describe( 'getCKEditorDependencies', () => {
-			it( 'should be defined', () => expect( tools.getCKEditorDependencies ).to.be.a( 'function' ) );
-
-			it( 'should return null if no CKEditor5 repository is found', () => {
-				const dependencies = {
-					'plugin1': '',
-					'plugin2': '',
-					'plugin3': ''
-				};
-				expect( tools.getCKEditorDependencies( dependencies ) ).to.equal( null );
-				expect( tools.getCKEditorDependencies() ).to.equal( null );
-			} );
-
-			it( 'should return only ckeditor5- dependencies', () => {
-				const dependencies = {
-					'plugin1': '',
-					'ckeditor5-plugin-image': 'ckeditor/ckeditor5-plugin-image',
-					'plugin2': '',
-					'ckeditor5-core': 'ckeditor/ckeditor5-core'
-				};
-				const ckeditorDependencies = tools.getCKEditorDependencies( dependencies );
-
-				expect( ckeditorDependencies ).to.be.an( 'object' );
-				expect( ckeditorDependencies.plugin1 ).to.be.a( 'undefined' );
-				expect( ckeditorDependencies.plugin2 ).to.be.a( 'undefined' );
-				expect( ckeditorDependencies[ 'ckeditor5-plugin-image' ] ).to.be.a( 'string' );
-				expect( ckeditorDependencies[ 'ckeditor5-core' ] ).to.be.a( 'string' );
-			} );
-		} );
-
 		describe( 'getDirectories', () => {
 			it( 'should be defined', () => expect( tools.getDirectories ).to.be.a( 'function' ) );
 
@@ -297,21 +267,6 @@ describe( 'utils', () => {
 			} );
 		} );
 
-		describe( 'getCKE5Directories', () => {
-			it( 'should be defined', () => expect( tools.getCKE5Directories ).to.be.a( 'function' ) );
-
-			it( 'should return only ckeditor5 directories', () => {
-				const workspacePath = '/workspace/path';
-				const sourceDirectories = [ 'tools', 'ckeditor5', 'ckeditor5-core', '.bin', 'ckeditor5-plugin-image' ];
-				sandbox.stub( tools, 'getDirectories', () => sourceDirectories );
-				const directories = tools.getCKE5Directories( workspacePath );
-
-				expect( directories.length ).equal( 2 );
-				expect( directories[ 0 ] ).equal( 'ckeditor5-core' );
-				expect( directories[ 1 ] ).equal( 'ckeditor5-plugin-image' );
-			} );
-		} );
-
 		describe( 'updateJSONFile', () => {
 			it( 'should be defined', () => expect( tools.updateJSONFile ).to.be.a( 'function' ) );
 			it( 'should read, update and save JSON file', () => {
@@ -498,20 +453,6 @@ describe( 'utils', () => {
 			} );
 		} );
 
-		describe( 'getCKE5Symlinks', () => {
-			it( 'should return CKE5 symlinks from provided path', () => {
-				const fs = require( 'fs' );
-				const path = 'path/to/dir';
-				sandbox.stub( fs, 'readdirSync' ).returns( [ 'ckeditor5-core', 'ckeditor5-image', 'other-dependency' ] );
-				sandbox.stub( tools, 'isSymlink' ).returns( true );
-
-				const symlinks = tools.getCKE5Symlinks( path );
-				expect( symlinks.length ).to.equal( 2 );
-				expect( symlinks[ 0 ] ).to.equal( 'ckeditor5-core' );
-				expect( symlinks[ 1 ] ).to.equal( 'ckeditor5-image' );
-			} );
-		} );
-
 		describe( 'removeSymlink', () => {
 			it( 'should unlink provided symlink', () => {
 				const fs = require( 'fs' );

+ 99 - 0
dev/utils/ckeditor5-dirs.js

@@ -0,0 +1,99 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const path = require( 'path' );
+const tools = require( './tools' );
+const git = require( './git' );
+
+const dependencyRegExp = /^ckeditor5-/;
+
+module.exports = {
+	/**
+	 * Returns dependencies that starts with `ckeditor5-`, and have valid, short GitHub url. Returns `null` if no
+	 * dependencies are found.
+	 *
+	 * @param {Object} dependencies Dependencies object loaded from `package.json` file.
+	 * @returns {Object|null}
+	 */
+	getDependencies( dependencies ) {
+		let result = null;
+
+		if ( dependencies ) {
+			Object.keys( dependencies ).forEach( ( key ) => {
+				if ( dependencyRegExp.test( key ) ) {
+					if ( result === null ) {
+						result = {};
+					}
+
+					result[ key ] = dependencies[ key ];
+				}
+			} );
+		}
+
+		return result;
+	},
+
+	/**
+	 * Returns all directories under specified path that match `ckeditor5-*` pattern.
+	 *
+	 * @param {String} path
+	 * @returns {Array.<String>}
+	 */
+	getDirectories( path ) {
+		return tools.getDirectories( path ).filter( dir => {
+			return dependencyRegExp.test( dir );
+		} );
+	},
+
+	/**
+	 * Returns a list of symbolic links to directories with names starting with `ckeditor5-` prefix.
+	 *
+	 * @param {String} path Path to directory,
+	 * @returns {Array} Array with directories names.
+	 */
+	getSymlinks( path ) {
+		const fs = require( 'fs' );
+		const pth = require( 'path' );
+
+		return fs.readdirSync( path ).filter( item => {
+			const fullPath = pth.join( path, item );
+
+			return dependencyRegExp.test( item ) && tools.isSymlink( fullPath );
+		} );
+	},
+
+	/**
+	 * Returns an array with information about `ckeditor5-*` directories in development mode.
+	 *
+	 * @param {String} workspacePath Absolute path to workspace.
+	 * @param {Object} packageJSON Contents of `ckeditor5` repo `package.json` file.
+	 * @param {String} ckeditor5Path Absolute path to ckeditor5 root directory.
+	 * @returns {Array.<Object>}
+	 */
+	getDevDirectories( workspacePath, packageJSON, ckeditor5Path ) {
+		const directories = this.getDirectories( workspacePath );
+		const dependencies = this.getDependencies( packageJSON.dependencies );
+
+		let devDirectories = [];
+
+		for ( let dependency in dependencies ) {
+			const repositoryURL = dependencies[ dependency ];
+			const urlInfo = git.parseRepositoryUrl( repositoryURL );
+			const repositoryPath = path.join( ckeditor5Path, 'node_modules', dependency );
+
+			// Check if repository's directory already exists.
+			if ( directories.indexOf( urlInfo.name ) > -1 ) {
+				devDirectories.push( {
+					repositoryPath,
+					repositoryURL
+				} );
+			}
+		}
+
+		return devDirectories;
+	}
+};

+ 29 - 1
dev/tasks/dev/utils/git.js → dev/utils/git.js

@@ -9,7 +9,6 @@ const tools = require( './tools' );
 const defaultOrigin = 'origin';
 
 module.exports = {
-
 	/**
 	 * Parses GitHub URL. Extracts used server, repository and branch.
 	 *
@@ -158,5 +157,34 @@ module.exports = {
 		];
 
 		tools.shExec( addRemoteCommands.join( ' && ' ) );
+	},
+
+	/*
+	 * Creates commit on repository under specified path
+	 *
+	 * @param {String} message
+	 * @param {String} repositoryPath
+	 */
+	commit( message, repositoryPath ) {
+		const commitCommands = [
+			`cd ${ repositoryPath }`,
+			`git commit --all --message "${ message }"`
+		];
+
+		tools.shExec( commitCommands.join( ' && ' ) );
+	},
+
+	/**
+	 * Pushes changes to repository's default location
+	 *
+	 * @param {String} repositoryPath
+	 */
+	push( repositoryPath ) {
+		const pushCommands = [
+			`cd ${ repositoryPath }`,
+			`git push`
+		];
+
+		tools.shExec( pushCommands.join( ' && ' ) );
 	}
 };

+ 4 - 2
dev/tasks/dev/utils/log.js → dev/utils/log.js

@@ -5,8 +5,10 @@
 
 'use strict';
 
-let logOut;
-let logErr;
+const gutil = require( 'gulp-util' );
+
+let logOut = ( msg ) => gutil.log( msg );
+let logErr = ( msg ) => gutil.log( gutil.colors.red( msg ) );
 
 module.exports = {
 	/**

+ 17 - 56
dev/tasks/dev/utils/tools.js → dev/utils/tools.js

@@ -6,11 +6,10 @@
 'use strict';
 
 const gutil = require( 'gulp-util' );
-
-const dependencyRegExp = /^ckeditor5-/;
+const path = require( 'path' );
+const del = require( 'del' );
 
 module.exports = {
-
 	/**
 	 * Executes a shell command.
 	 *
@@ -62,31 +61,6 @@ module.exports = {
 		fs.symlinkSync( source, destination, 'dir' );
 	},
 
-	/**
-	 * Returns dependencies that starts with ckeditor5-, and have valid, short GitHub url. Returns null if no
-	 * dependencies are found.
-	 *
-	 * @param {Object} dependencies Dependencies object loaded from package.json file.
-	 * @returns {Object|null}
-	 */
-	getCKEditorDependencies( dependencies ) {
-		let result = null;
-
-		if ( dependencies ) {
-			Object.keys( dependencies ).forEach( function( key ) {
-				if ( dependencyRegExp.test( key ) ) {
-					if ( result === null ) {
-						result = {};
-					}
-
-					result[ key ] = dependencies[ key ];
-				}
-			} );
-		}
-
-		return result;
-	},
-
 	/**
 	 * Returns array with all directories under specified path.
 	 *
@@ -149,18 +123,6 @@ module.exports = {
 		return false;
 	},
 
-	/**
-	 * Returns all directories under specified path that match 'ckeditor5' pattern.
-	 *
-	 * @param {String} path
-	 * @returns {Array}
-	 */
-	getCKE5Directories( path ) {
-		return this.getDirectories( path ).filter( dir => {
-			return dependencyRegExp.test( dir );
-		} );
-	},
-
 	/**
 	 * Updates JSON file under specified path.
 	 * @param {String} path Path to file on disk.
@@ -301,29 +263,28 @@ module.exports = {
 	},
 
 	/**
-	 * Returns list of symbolic links to directories with names starting with `ckeditor5-` prefix.
+	 * Unlinks symbolic link under specified path.
 	 *
-	 * @param {String} path Path to directory,
-	 * @returns {Array} Array with directories names.
+	 * @param {String} path
 	 */
-	getCKE5Symlinks( path ) {
+	removeSymlink( path ) {
 		const fs = require( 'fs' );
-		const pth = require( 'path' );
-
-		return fs.readdirSync( path ).filter( item => {
-			const fullPath = pth.join( path, item );
-
-			return dependencyRegExp.test( item ) && this.isSymlink( fullPath );
-		} );
+		fs.unlinkSync( path );
 	},
 
 	/**
-	 * Unlinks symbolic link under specified path.
+	 * Removes files and directories specified by `glob` starting from `rootDir`
+	 * and gently informs about deletion.
 	 *
-	 * @param {String} path
+	 * @param {String} rootDir The path to the root directory (i.e. "dist/").
+	 * @param {String} glob Glob specifying what to clean.
+	 * @returns {Promise}
 	 */
-	removeSymlink( path ) {
-		const fs = require( 'fs' );
-		fs.unlinkSync( path );
+	clean( rootDir, glob ) {
+		return del( path.join( rootDir, glob ) ).then( paths => {
+			paths.forEach( p => {
+				gutil.log( `Deleted file '${ gutil.colors.cyan( p ) }'.` );
+			} );
+		} );
 	}
 };

+ 1 - 0
gulpfile.js

@@ -22,6 +22,7 @@ require( './dev/tasks/dev/tasks' )( config ).register();
 require( './dev/tasks/lint/tasks' )( config ).register();
 require( './dev/tasks/test/tasks' )( config ).register();
 require( './dev/tasks/docs/tasks' )( config ).register();
+require( './dev/tasks/exec/tasks' )( config ).register();
 
 gulp.task( 'default', [ 'build' ] );
 gulp.task( 'pre-commit', [ 'lint-staged' ] );

+ 1 - 0
package.json

@@ -72,6 +72,7 @@
     "multipipe": "^0.3.0",
     "ncp": "^2.0.0",
     "node-sass": "^3.7.0",
+    "parse-gitignore": "^0.2.0",
     "pretty-bytes": "^3.0.1",
     "regenerator-runtime": "^0.9.5",
     "replace": "^0.3.0",