forked from CIT/Vmeda.Online
		
	
						commit
						c77c620964
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -46,3 +46,4 @@ e2e/build | |||||||
| !/desktop/assets/ | !/desktop/assets/ | ||||||
| !/desktop/electron.js | !/desktop/electron.js | ||||||
| src/configconstants.ts | src/configconstants.ts | ||||||
|  | .moodleapp-dev-config | ||||||
|  | |||||||
| @ -64,6 +64,7 @@ jobs: | |||||||
|     - node --version |     - node --version | ||||||
|     - npm --version |     - npm --version | ||||||
|     - nvm --version |     - nvm --version | ||||||
|  |     - sudo apt-get install -y libsecret-1-dev > /dev/null | ||||||
|     - npm ci |     - npm ci | ||||||
|     - npm install -g gulp |     - npm install -g gulp | ||||||
|     script: scripts/aot.sh |     script: scripts/aot.sh | ||||||
| @ -83,6 +84,8 @@ jobs: | |||||||
|     - ELECTRON_CACHE=$HOME/.cache/electron |     - ELECTRON_CACHE=$HOME/.cache/electron | ||||||
|     - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder |     - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder | ||||||
|     - BUILD_PLATFORM='linux' |     - BUILD_PLATFORM='linux' | ||||||
|  |     before_install: | ||||||
|  |     - sudo apt-get install -y libsecret-1-dev > /dev/null | ||||||
|     script: scripts/aot.sh |     script: scripts/aot.sh | ||||||
|   - stage: build |   - stage: build | ||||||
|     name: "Build MacOS" |     name: "Build MacOS" | ||||||
|  | |||||||
							
								
								
									
										69
									
								
								gulp/dev-config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								gulp/dev-config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | const fs = require('fs'); | ||||||
|  | 
 | ||||||
|  | const DEV_CONFIG_FILE = '.moodleapp-dev-config'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Class to read and write dev-config data from a file. | ||||||
|  |  */ | ||||||
|  | class DevConfig { | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         this.loadFileData(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get a setting. | ||||||
|  |      * | ||||||
|  |      * @param name Name of the setting to get. | ||||||
|  |      * @param defaultValue Value to use if not found. | ||||||
|  |      */ | ||||||
|  |     get(name, defaultValue) { | ||||||
|  |         return typeof this.config[name] != 'undefined' ? this.config[name] : defaultValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load file data to memory. | ||||||
|  |      */ | ||||||
|  |     loadFileData() { | ||||||
|  |         if (!fs.existsSync(DEV_CONFIG_FILE)) { | ||||||
|  |             this.config = {}; | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             this.config = JSON.parse(fs.readFileSync(DEV_CONFIG_FILE)); | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error('Error reading dev config file.', error); | ||||||
|  |             this.config = {}; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Save some settings. | ||||||
|  |      * | ||||||
|  |      * @param settings Object with the settings to save. | ||||||
|  |      */ | ||||||
|  |     save(settings) { | ||||||
|  |         this.config = Object.assign(this.config, settings); | ||||||
|  | 
 | ||||||
|  |         // Save the data in the dev file.
 | ||||||
|  |         fs.writeFileSync(DEV_CONFIG_FILE, JSON.stringify(this.config, null, 4)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = new DevConfig(); | ||||||
							
								
								
									
										237
									
								
								gulp/git.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								gulp/git.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,237 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | const exec = require('child_process').exec; | ||||||
|  | const fs = require('fs'); | ||||||
|  | const DevConfig = require('./dev-config'); | ||||||
|  | const Utils = require('./utils'); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Class to run git commands. | ||||||
|  |  */ | ||||||
|  | class Git { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a patch. | ||||||
|  |      * | ||||||
|  |      * @param range Show only commits in the specified revision range. | ||||||
|  |      * @param saveTo Path to the file to save the patch to. If not defined, the patch contents will be returned. | ||||||
|  |      * @return Promise resolved when done. If saveTo not provided, it will return the patch contents. | ||||||
|  |      */ | ||||||
|  |     createPatch(range, saveTo) { | ||||||
|  |         return new Promise((resolve, reject) => { | ||||||
|  |             exec(`git format-patch ${range} --stdout`, (err, result) => { | ||||||
|  |                 if (err) { | ||||||
|  |                     reject(err || 'Cannot create patch.'); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (!saveTo) { | ||||||
|  |                     resolve(result); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Save it to a file.
 | ||||||
|  |                 const directory = saveTo.substring(0, saveTo.lastIndexOf('/')); | ||||||
|  |                 if (directory && directory != '.' && directory != '..' && !fs.existsSync(directory)) { | ||||||
|  |                     fs.mkdirSync(directory); | ||||||
|  |                 } | ||||||
|  |                 fs.writeFileSync(saveTo, result); | ||||||
|  | 
 | ||||||
|  |                 resolve(); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get current branch. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved with the branch name. | ||||||
|  |      */ | ||||||
|  |     getCurrentBranch() { | ||||||
|  |         return new Promise((resolve, reject) => { | ||||||
|  |             exec('git branch --show-current', (err, branch) => { | ||||||
|  |                 if (branch) { | ||||||
|  |                     resolve(branch.replace('\n', '')); | ||||||
|  |                 } else { | ||||||
|  |                     reject (err || 'Current branch not found.'); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the HEAD commit for a certain branch. | ||||||
|  |      * | ||||||
|  |      * @param branch Name of the branch. | ||||||
|  |      * @param branchData Parsed branch data. If not provided it will be calculated. | ||||||
|  |      * @return HEAD commit. | ||||||
|  |      */ | ||||||
|  |     async getHeadCommit(branch, branchData) { | ||||||
|  |         if (!branchData) { | ||||||
|  |             // Parse the branch to get the project and issue number.
 | ||||||
|  |             branchData = Utils.parseBranch(branch); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Loop over the last commits to find the first commit messages that doesn't belong to the issue.
 | ||||||
|  |         const commitsString = await this.log(50, branch, '%s_____%H'); | ||||||
|  |         const commits = commitsString.split('\n'); | ||||||
|  |         commits.pop(); // Remove last element, it's an empty string.
 | ||||||
|  | 
 | ||||||
|  |         for (let i = 0; i < commits.length; i++) { | ||||||
|  |             const commit = commits[i]; | ||||||
|  |             const match = Utils.getIssueFromCommitMessage(commit) == branchData.issue; | ||||||
|  | 
 | ||||||
|  |             if (i === 0 && !match) { | ||||||
|  |                 // Most recent commit doesn't belong to the issue. Stop looking.
 | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!match) { | ||||||
|  |                 // The commit does not match any more, we found it!
 | ||||||
|  |                 return commit.split('_____')[1]; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Couldn't find the commit using the commit names, get the last commit in the integration branch.
 | ||||||
|  |         const remote = DevConfig.get('upstreamRemote', 'origin'); | ||||||
|  |         console.log(`Head commit not found using commit messages. Get last commit from ${remote}/integration`); | ||||||
|  |         const hashes = await this.hashes(1, `${remote}/integration`); | ||||||
|  | 
 | ||||||
|  |         return hashes[0]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the URL of a certain remote. | ||||||
|  |      * | ||||||
|  |      * @param remote Remote name. | ||||||
|  |      * @return Promise resolved with the remote URL. | ||||||
|  |      */ | ||||||
|  |     getRemoteUrl(remote) { | ||||||
|  |         return new Promise((resolve, reject) => { | ||||||
|  |             exec(`git remote get-url ${remote}`, (err, url) => { | ||||||
|  |                 if (url) { | ||||||
|  |                     resolve(url.replace('\n', '')); | ||||||
|  |                 } else { | ||||||
|  |                     reject (err || 'Remote not found.'); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the latest hashes from git log. | ||||||
|  |      * | ||||||
|  |      * @param count Number of commits to display. | ||||||
|  |      * @param range Show only commits in the specified revision range. | ||||||
|  |      * @param format Pretty-print the contents of the commit logs in a given format. | ||||||
|  |      * @return Promise resolved with the list of hashes. | ||||||
|  |      */ | ||||||
|  |     async hashes(count, range, format) { | ||||||
|  |         format = format || '%H'; | ||||||
|  | 
 | ||||||
|  |         const hashList = await this.log(count, range, format); | ||||||
|  | 
 | ||||||
|  |         const hashes = hashList.split('\n'); | ||||||
|  |         hashes.pop(); // Remove last element, it's an empty string.
 | ||||||
|  | 
 | ||||||
|  |         return hashes; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calls the log command and returns the raw output. | ||||||
|  |      * | ||||||
|  |      * @param count Number of commits to display. | ||||||
|  |      * @param range Show only commits in the specified revision range. | ||||||
|  |      * @param format Pretty-print the contents of the commit logs in a given format. | ||||||
|  |      * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. | ||||||
|  |      * @return Promise resolved with the result. | ||||||
|  |      */ | ||||||
|  |     log(count, range, format, path) { | ||||||
|  |         if (typeof count == 'undefined') { | ||||||
|  |             count = 10; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let command = 'git log'; | ||||||
|  | 
 | ||||||
|  |         if (count > 0) { | ||||||
|  |             command += ` -n ${count} `; | ||||||
|  |         } | ||||||
|  |         if (format) { | ||||||
|  |             command += ` --format=${format} `; | ||||||
|  |         } | ||||||
|  |         if (range){ | ||||||
|  |             command += ` ${range} `; | ||||||
|  |         } | ||||||
|  |         if (path) { | ||||||
|  |             command += ` -- ${path}`; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return new Promise((resolve, reject) => { | ||||||
|  |             exec(command, (err, result, stderr) => { | ||||||
|  |                 if (err) { | ||||||
|  |                     reject(err); | ||||||
|  |                 } else { | ||||||
|  |                     resolve(result); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the latest titles of the commit messages. | ||||||
|  |      * | ||||||
|  |      * @param count Number of commits to display. | ||||||
|  |      * @param range Show only commits in the specified revision range. | ||||||
|  |      * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. | ||||||
|  |      * @return Promise resolved with the list of titles. | ||||||
|  |      */ | ||||||
|  |     async messages(count, range, path) { | ||||||
|  |         count = typeof count != 'undefined' ? count : 10; | ||||||
|  | 
 | ||||||
|  |         const messageList = await this.log(count, range, '%s', path); | ||||||
|  | 
 | ||||||
|  |         const messages = messageList.split('\n'); | ||||||
|  |         messages.pop(); // Remove last element, it's an empty string.
 | ||||||
|  | 
 | ||||||
|  |         return messages; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Push a branch. | ||||||
|  |      * | ||||||
|  |      * @param remote Remote to use. | ||||||
|  |      * @param branch Branch to push. | ||||||
|  |      * @param force Whether to force the push. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     push(remote, branch, force) { | ||||||
|  |         return new Promise((resolve, reject) => { | ||||||
|  |             let command = `git push ${remote} ${branch}`; | ||||||
|  |             if (force) { | ||||||
|  |                 command += ' -f'; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             exec(command, (err, result, stderr) => { | ||||||
|  |                 if (err) { | ||||||
|  |                     reject(err); | ||||||
|  |                 } else { | ||||||
|  |                     resolve(); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = new Git(); | ||||||
							
								
								
									
										474
									
								
								gulp/jira.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										474
									
								
								gulp/jira.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,474 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | const exec = require('child_process').exec; | ||||||
|  | const https = require('https'); | ||||||
|  | const keytar = require('keytar'); | ||||||
|  | const inquirer = require('inquirer'); | ||||||
|  | const fs = require('fs'); | ||||||
|  | const request = require('request'); // This lib is deprecated, but it was the only way I found to make upload files work.
 | ||||||
|  | const DevConfig = require('./dev-config'); | ||||||
|  | const Git = require('./git'); | ||||||
|  | const Url = require('./url'); | ||||||
|  | const Utils = require('./utils'); | ||||||
|  | 
 | ||||||
|  | const apiVersion = 2; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Class to interact with Jira. | ||||||
|  |  */ | ||||||
|  | class Jira { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Ask the password to the user. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved with the password. | ||||||
|  |      */ | ||||||
|  |     async askPassword() { | ||||||
|  |         const data = await inquirer.prompt([ | ||||||
|  |             { | ||||||
|  |                 type: 'password', | ||||||
|  |                 name: 'password', | ||||||
|  |                 message: `Please enter the password for the username ${this.username}.`, | ||||||
|  |             }, | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |         return data.password; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Ask the user the tracker data. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved with the data, rejected if cannot get. | ||||||
|  |      */ | ||||||
|  |     async askTrackerData() { | ||||||
|  |         const data = await inquirer.prompt([ | ||||||
|  |             { | ||||||
|  |                 type: 'input', | ||||||
|  |                 name: 'url', | ||||||
|  |                 message: 'Please enter the tracker URL.', | ||||||
|  |                 default: 'https://tracker.moodle.org/', | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 type: 'input', | ||||||
|  |                 name: 'username', | ||||||
|  |                 message: 'Please enter your tracker username.', | ||||||
|  |             }, | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |         DevConfig.save({ | ||||||
|  |             'tracker.url': data.url, | ||||||
|  |             'tracker.username': data.username, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Build URL to perform requests to Jira. | ||||||
|  |      * | ||||||
|  |      * @param uri URI to add the the Jira URL. | ||||||
|  |      * @return URL. | ||||||
|  |      */ | ||||||
|  |     buildRequestUrl(uri) { | ||||||
|  |         return Utils.concatenatePaths([this.url, this.uri, '/rest/api/', apiVersion, uri]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Delete an attachment. | ||||||
|  |      * | ||||||
|  |      * @param attachmentId Attachment ID. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async deleteAttachment(attachmentId) { | ||||||
|  |         const response = await this.request(`attachment/${attachmentId}`, 'DELETE'); | ||||||
|  | 
 | ||||||
|  |         if (response.status != 204) { | ||||||
|  |             throw new Error('Could not delete the attachment'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load the issue info from jira server using a REST API call. | ||||||
|  |      * | ||||||
|  |      * @param key Key to identify the issue. E.g. MOBILE-1234. | ||||||
|  |      * @param fields Fields to get. | ||||||
|  |      * @return Promise resolved with the issue data. | ||||||
|  |      */ | ||||||
|  |     async getIssue(key, fields) { | ||||||
|  |         fields = fields || '*all,-comment'; | ||||||
|  | 
 | ||||||
|  |         await this.init(); // Initialize data if needed.
 | ||||||
|  | 
 | ||||||
|  |         const response = await this.request(`issue/${key}`, 'GET', {'fields': fields, 'expand': 'names'}); | ||||||
|  | 
 | ||||||
|  |         if (response.status == 404) { | ||||||
|  |             throw new Error('Issue could not be found.'); | ||||||
|  |         } else if (response.status != 200) { | ||||||
|  |             throw new Error('The tracker is not available.') | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const issue = response.data; | ||||||
|  |         issue.named = {}; | ||||||
|  | 
 | ||||||
|  |         // Populate the named fields in a separate key. Allows us to easily find them without knowing the field ID.
 | ||||||
|  |         const nameList = issue.names || {}; | ||||||
|  |         for (const fieldKey in issue.fields) { | ||||||
|  |             if (nameList[fieldKey]) { | ||||||
|  |                 issue.named[nameList[fieldKey]] = issue.fields[fieldKey]; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return issue | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load the version info from the jira server using a rest api call. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async getServerInfo() { | ||||||
|  |         const response = await this.request('serverInfo'); | ||||||
|  | 
 | ||||||
|  |         if (response.status != 200) { | ||||||
|  |             throw new Error(`Unexpected response code: ${response.status}`, response); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.version = response.data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get tracker data to push an issue. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved with the data. | ||||||
|  |      */ | ||||||
|  |     async getTrackerData() { | ||||||
|  |         // Check dev-config file first.
 | ||||||
|  |         let data = this.getTrackerDataFromDevConfig(); | ||||||
|  | 
 | ||||||
|  |         if (data) { | ||||||
|  |             console.log('Using tracker data from dev-config file'); | ||||||
|  |             return data; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Try to use mdk now.
 | ||||||
|  |         try { | ||||||
|  |             data = await this.getTrackerDataFromMdk(); | ||||||
|  | 
 | ||||||
|  |             console.log('Using tracker data from mdk'); | ||||||
|  | 
 | ||||||
|  |             return data; | ||||||
|  |         } catch (error) { | ||||||
|  |             // MDK not available or not configured. Ask for the data.
 | ||||||
|  |             const data = await this.askTrackerData(); | ||||||
|  | 
 | ||||||
|  |             data.fromInput = true; | ||||||
|  | 
 | ||||||
|  |             return data; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get tracker data from dev config file. | ||||||
|  |      * | ||||||
|  |      * @return Data, undefined if cannot get. | ||||||
|  |      */ | ||||||
|  |     getTrackerDataFromDevConfig() { | ||||||
|  |         const url = DevConfig.get('tracker.url'); | ||||||
|  |         const username = DevConfig.get('tracker.username'); | ||||||
|  | 
 | ||||||
|  |         if (url && username) { | ||||||
|  |             return { | ||||||
|  |                 url, | ||||||
|  |                 username, | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get tracker URL and username from mdk. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved with the data, rejected if cannot get. | ||||||
|  |      */ | ||||||
|  |     getTrackerDataFromMdk() { | ||||||
|  |         return new Promise((resolve, reject) => { | ||||||
|  |             exec('mdk config show tracker.url', (err, url) => { | ||||||
|  |                 if (!url) { | ||||||
|  |                     reject(err || 'URL not found.'); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 exec('mdk config show tracker.username', (err, username) => { | ||||||
|  |                     if (username) { | ||||||
|  |                         resolve({ | ||||||
|  |                             url: url.replace('\n', ''), | ||||||
|  |                             username: username.replace('\n', ''), | ||||||
|  |                         }); | ||||||
|  |                     } else { | ||||||
|  |                         reject(err | 'Username not found.'); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize some data. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async init() { | ||||||
|  |         if (this.initialized) { | ||||||
|  |             // Already initialized.
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Get tracker URL and username.
 | ||||||
|  |         const trackerData = await this.getTrackerData(); | ||||||
|  | 
 | ||||||
|  |         this.url = trackerData.url; | ||||||
|  |         this.username = trackerData.username; | ||||||
|  | 
 | ||||||
|  |         const parsed = Url.parse(this.url); | ||||||
|  |         this.ssl = parsed.protocol == 'https'; | ||||||
|  |         this.host = parsed.domain; | ||||||
|  |         this.uri = parsed.path; | ||||||
|  | 
 | ||||||
|  |         // Get the password.
 | ||||||
|  |         this.password = await keytar.getPassword('mdk-jira-password', this.username); // Use same service name as mdk.
 | ||||||
|  | 
 | ||||||
|  |         if (!this.password) { | ||||||
|  |             // Ask the user.
 | ||||||
|  |             this.password = await this.askPassword(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         while (!this.initialized) { | ||||||
|  |             try { | ||||||
|  |                 await this.getServerInfo(); | ||||||
|  | 
 | ||||||
|  |                 this.initialized = true; | ||||||
|  |                 keytar.setPassword('mdk-jira-password', this.username, this.password); | ||||||
|  |             } catch (error) { | ||||||
|  |                 console.log('Error connecting to the server. Please make sure you entered the data correctly.', error); | ||||||
|  |                 if (trackerData.fromInput) { | ||||||
|  |                     // User entered the data manually, ask him again.
 | ||||||
|  |                     trackerData = await this.askTrackerData(); | ||||||
|  | 
 | ||||||
|  |                     this.url = trackerData.url; | ||||||
|  |                     this.username = trackerData.username; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 this.password = await this.askPassword(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if a certain issue could be a security issue. | ||||||
|  |      * | ||||||
|  |      * @param key Key to identify the issue. E.g. MOBILE-1234. | ||||||
|  |      * @return Promise resolved with boolean: whether it's a security issue. | ||||||
|  |      */ | ||||||
|  |     async isSecurityIssue(key) { | ||||||
|  |         const issue = await this.getIssue(key, 'security'); | ||||||
|  | 
 | ||||||
|  |         return issue.fields && !!issue.fields.security; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sends a request to the server and returns the data. | ||||||
|  |      * | ||||||
|  |      * @param uri URI to add the the Jira URL. | ||||||
|  |      * @param method Method to use. Defaults to 'GET'. | ||||||
|  |      * @param params Params to send as GET params (in the URL). | ||||||
|  |      * @param data JSON string with the data to send as POST/PUT params. | ||||||
|  |      * @param headers Headers to send. | ||||||
|  |      * @return Promise resolved with the result. | ||||||
|  |      */ | ||||||
|  |     request(uri, method, params, data, headers) { | ||||||
|  |         uri = uri || ''; | ||||||
|  |         method = (method || 'GET').toUpperCase(); | ||||||
|  |         data = data || ''; | ||||||
|  |         params = params || {}; | ||||||
|  |         headers = headers || {}; | ||||||
|  |         headers['Content-Type'] = 'application/json'; | ||||||
|  | 
 | ||||||
|  |         return new Promise((resolve, reject) => { | ||||||
|  | 
 | ||||||
|  |             // Build the request URL.
 | ||||||
|  |             const url = Url.addParamsToUrl(this.buildRequestUrl(uri), params); | ||||||
|  | 
 | ||||||
|  |             // Initialize the request.
 | ||||||
|  |             const options = { | ||||||
|  |                 method: method, | ||||||
|  |                 auth: `${this.username}:${this.password}`, | ||||||
|  |                 headers: headers, | ||||||
|  |             }; | ||||||
|  |             const request = https.request(url, options); | ||||||
|  | 
 | ||||||
|  |             // Add data.
 | ||||||
|  |             if (data) { | ||||||
|  |                 request.write(data); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Treat response.
 | ||||||
|  |             request.on('response', (response) => { | ||||||
|  |                 // Read the result.
 | ||||||
|  |                 let result = ''; | ||||||
|  |                 response.on('data', (chunk) => { | ||||||
|  |                     result += chunk; | ||||||
|  |                 }); | ||||||
|  |                 response.on('end', () => { | ||||||
|  |                     try { | ||||||
|  |                         result = JSON.parse(result); | ||||||
|  |                     } catch (error) { | ||||||
|  |                         // Leave it as text.
 | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     resolve({ | ||||||
|  |                         status: response.statusCode, | ||||||
|  |                         data: result, | ||||||
|  |                     }); | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             request.on('error', (e) => { | ||||||
|  |                 reject(e); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             // Send the request.
 | ||||||
|  |             request.end(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sets a set of fields for a certain issue in Jira. | ||||||
|  |      * | ||||||
|  |      * @param key Key to identify the issue. E.g. MOBILE-1234. | ||||||
|  |      * @param updates Object with the fields to update. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async setCustomFields(key, updates) { | ||||||
|  |         const issue = await this.getIssue(key); | ||||||
|  |         const update = {'fields': {}}; | ||||||
|  | 
 | ||||||
|  |         // Detect which fields have changed.
 | ||||||
|  |         for (const updateName in updates) { | ||||||
|  |             const updateValue = updates[updateName]; | ||||||
|  |             const remoteValue = issue.named[updateName]; | ||||||
|  | 
 | ||||||
|  |             if (!remoteValue || remoteValue != updateValue) { | ||||||
|  |                 // Map the label of the field with the field code.
 | ||||||
|  |                 let fieldKey; | ||||||
|  |                 for (const key in issue.names) { | ||||||
|  |                     if (issue.names[key] == updateName) { | ||||||
|  |                         fieldKey = key; | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (!fieldKey) { | ||||||
|  |                     throw new Error(`Could not find the field named ${updateName}.`); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 update.fields[fieldKey] = updateValue; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!Object.keys(update.fields).length) { | ||||||
|  |             // No fields to update.
 | ||||||
|  |             console.log('No updates required.') | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const response = await this.request(`issue/${key}`, 'PUT', null, JSON.stringify(update)); | ||||||
|  | 
 | ||||||
|  |         if (response.status != 204) { | ||||||
|  |             throw new Error(`Issue was not updated: ${response.status}`, response.data); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         console.log('Issue updated successfully.'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Upload a new attachment to an issue. | ||||||
|  |      * | ||||||
|  |      * @param key Key to identify the issue. E.g. MOBILE-1234. | ||||||
|  |      * @param filePath Path to the file to upload. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async upload(key, filePath) { | ||||||
|  | 
 | ||||||
|  |         const uri = `issue/${key}/attachments`; | ||||||
|  |         const headers = { | ||||||
|  |             'X-Atlassian-Token': 'nocheck', | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const response = await this.uploadFile(uri, 'file', filePath, headers); | ||||||
|  | 
 | ||||||
|  |         if (response.status != 200) { | ||||||
|  |             throw new Error('Could not upload file to Jira issue'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         console.log('File successfully uploaded.') | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Upload a file to Jira. | ||||||
|  |      * | ||||||
|  |      * @param uri URI to add the the Jira URL. | ||||||
|  |      * @param fieldName Name of the form field where to put the file. | ||||||
|  |      * @param filePath Path to the file. | ||||||
|  |      * @param headers Headers. | ||||||
|  |      * @return Promise resolved with the result. | ||||||
|  |      */ | ||||||
|  |     async uploadFile(uri, fieldName, filePath, headers) { | ||||||
|  |         uri = uri || ''; | ||||||
|  |         headers = headers || {}; | ||||||
|  |         headers['Content-Type'] = 'multipart/form-data'; | ||||||
|  | 
 | ||||||
|  |         return new Promise((resolve, reject) => { | ||||||
|  |             // Add the file to the form data.
 | ||||||
|  |             const formData = {}; | ||||||
|  |             formData[fieldName] = { | ||||||
|  |                 value: fs.createReadStream(filePath), | ||||||
|  |                 options: { | ||||||
|  |                     filename: filePath.substr(filePath.lastIndexOf('/') + 1), | ||||||
|  |                     contentType: 'multipart/form-data', | ||||||
|  |                 }, | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             // Perform the request.
 | ||||||
|  |             const options = { | ||||||
|  |                 url: this.buildRequestUrl(uri), | ||||||
|  |                 method: 'POST', | ||||||
|  |                 headers: headers, | ||||||
|  |                 auth: { | ||||||
|  |                     user: this.username, | ||||||
|  |                     pass: this.password, | ||||||
|  |                 }, | ||||||
|  |                 formData: formData, | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             request(options, (err, httpResponse, body) => { | ||||||
|  |                 resolve({ | ||||||
|  |                     status: httpResponse.statusCode, | ||||||
|  |                     data: body, | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = new Jira(); | ||||||
							
								
								
									
										113
									
								
								gulp/task-build-config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								gulp/task-build-config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,113 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | const gulp = require('gulp'); | ||||||
|  | const through = require('through'); | ||||||
|  | const bufferFrom = require('buffer-from'); | ||||||
|  | const rename = require('gulp-rename'); | ||||||
|  | const exec = require('child_process').exec; | ||||||
|  | 
 | ||||||
|  | const LICENSE = '' + | ||||||
|  |         '// (C) Copyright 2015 Moodle Pty Ltd.\n' + | ||||||
|  |         '//\n' + | ||||||
|  |         '// Licensed under the Apache License, Version 2.0 (the "License");\n' + | ||||||
|  |         '// you may not use this file except in compliance with the License.\n' + | ||||||
|  |         '// You may obtain a copy of the License at\n' + | ||||||
|  |         '//\n' + | ||||||
|  |         '//     http://www.apache.org/licenses/LICENSE-2.0\n' + | ||||||
|  |         '//\n' + | ||||||
|  |         '// Unless required by applicable law or agreed to in writing, software\n' + | ||||||
|  |         '// distributed under the License is distributed on an "AS IS" BASIS,\n' + | ||||||
|  |         '// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' + | ||||||
|  |         '// See the License for the specific language governing permissions and\n' + | ||||||
|  |         '// limitations under the License.\n\n'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Task to convert config.json into a TypeScript class. | ||||||
|  |  */ | ||||||
|  | class BuildConfigTask { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Run the task. | ||||||
|  |      * | ||||||
|  |      * @param path Path to the config file. | ||||||
|  |      * @param done Function to call when done. | ||||||
|  |      */ | ||||||
|  |     run(path, done) { | ||||||
|  |         // Get the last commit.
 | ||||||
|  |         exec('git log -1 --pretty=format:"%H"', (err, commit, stderr) => { | ||||||
|  |             if (err) { | ||||||
|  |                 console.error('An error occurred while getting the last commit: ' + err); | ||||||
|  |             } else if (stderr) { | ||||||
|  |                 console.error('An error occurred while getting the last commit: ' + stderr); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             gulp.src(path) | ||||||
|  |                 .pipe(through(function(file) { | ||||||
|  |                     // Convert the contents of the file into a TypeScript class.
 | ||||||
|  |                     // Disable the rule variable-name in the file.
 | ||||||
|  |                     const config = JSON.parse(file.contents.toString()); | ||||||
|  |                     let contents = LICENSE + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n'; | ||||||
|  | 
 | ||||||
|  |                     for (let key in config) { | ||||||
|  |                         let value = config[key]; | ||||||
|  | 
 | ||||||
|  |                         if (typeof value == 'string') { | ||||||
|  |                             // Wrap the string in ' and escape them.
 | ||||||
|  |                             value = "'" + value.replace(/([^\\])'/g, "$1\\'") + "'"; | ||||||
|  |                         } else if (typeof value != 'number' && typeof value != 'boolean') { | ||||||
|  |                             // Stringify with 4 spaces of indentation, and then add 4 more spaces in each line.
 | ||||||
|  |                             value = JSON.stringify(value, null, 4).replace(/^(?:    )/gm, '        ').replace(/^(?:})/gm, '    }'); | ||||||
|  |                             // Replace " by ' in values.
 | ||||||
|  |                             value = value.replace(/: "([^"]*)"/g, ": '$1'"); | ||||||
|  | 
 | ||||||
|  |                             // Check if the keys have "-" in it.
 | ||||||
|  |                             const matches = value.match(/"([^"]*\-[^"]*)":/g); | ||||||
|  |                             if (matches) { | ||||||
|  |                                 // Replace " by ' in keys. We cannot remove them because keys have chars like '-'.
 | ||||||
|  |                                 value = value.replace(/"([^"]*)":/g, "'$1':"); | ||||||
|  |                             } else { | ||||||
|  |                                 // Remove ' in keys.
 | ||||||
|  |                                 value = value.replace(/"([^"]*)":/g, "$1:"); | ||||||
|  |                             } | ||||||
|  | 
 | ||||||
|  |                             // Add type any to the key.
 | ||||||
|  |                             key = key + ': any'; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         // If key has quotation marks, remove them.
 | ||||||
|  |                         if (key[0] == '"') { | ||||||
|  |                             key = key.substr(1, key.length - 2); | ||||||
|  |                         } | ||||||
|  |                         contents += '    static ' + key + ' = ' + value + ';\n'; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     // Add compilation info.
 | ||||||
|  |                     contents += '    static compilationtime = ' + Date.now() + ';\n'; | ||||||
|  |                     contents += '    static lastcommit = \'' + commit + '\';\n'; | ||||||
|  | 
 | ||||||
|  |                     contents += '}\n'; | ||||||
|  | 
 | ||||||
|  |                     file.contents = bufferFrom(contents); | ||||||
|  | 
 | ||||||
|  |                     this.emit('data', file); | ||||||
|  |                 })) | ||||||
|  |                 .pipe(rename('configconstants.ts')) | ||||||
|  |                 .pipe(gulp.dest('./src')) | ||||||
|  |                 .on('end', done); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = BuildConfigTask; | ||||||
							
								
								
									
										176
									
								
								gulp/task-build-lang.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								gulp/task-build-lang.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,176 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | const gulp = require('gulp'); | ||||||
|  | const slash = require('gulp-slash'); | ||||||
|  | const clipEmptyFiles = require('gulp-clip-empty-files'); | ||||||
|  | const through = require('through'); | ||||||
|  | const bufferFrom = require('buffer-from'); | ||||||
|  | const File = require('vinyl'); | ||||||
|  | const pathLib = require('path'); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Task to build the language files into a single file per language. | ||||||
|  |  */ | ||||||
|  | class BuildLangTask { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Copy a property from one object to another, adding a prefix to the key if needed. | ||||||
|  |      * | ||||||
|  |      * @param target Object to copy the properties to. | ||||||
|  |      * @param source Object to copy the properties from. | ||||||
|  |      * @param prefix Prefix to add to the keys. | ||||||
|  |      */ | ||||||
|  |     addProperties(target, source, prefix) { | ||||||
|  |         for (let property in source) { | ||||||
|  |             target[prefix + property] = source[property]; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Run the task. | ||||||
|  |      * | ||||||
|  |      * @param language Language to treat. | ||||||
|  |      * @param langPaths Paths to the possible language files. | ||||||
|  |      * @param done Function to call when done. | ||||||
|  |      */ | ||||||
|  |     run(language, langPaths, done) { | ||||||
|  |         const filename = language + '.json'; | ||||||
|  |         const data = {}; | ||||||
|  |         let firstFile = null; | ||||||
|  |         const self = this; | ||||||
|  | 
 | ||||||
|  |         const paths = langPaths.map((path) => { | ||||||
|  |             if (path.slice(-1) != '/') { | ||||||
|  |                 path = path + '/'; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return path + language + '.json'; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         gulp.src(paths, { allowEmpty: true }) | ||||||
|  |             .pipe(slash()) | ||||||
|  |             .pipe(clipEmptyFiles()) | ||||||
|  |             .pipe(through(function(file) { | ||||||
|  |                 if (!firstFile) { | ||||||
|  |                     firstFile = file; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return self.treatFile(file, data); | ||||||
|  |             }, function() { | ||||||
|  |                 /* This implementation is based on gulp-jsoncombine module. | ||||||
|  |                  * https://github.com/reflog/gulp-jsoncombine */
 | ||||||
|  |                 if (firstFile) { | ||||||
|  |                     const joinedPath = pathLib.join(firstFile.base, language + '.json'); | ||||||
|  | 
 | ||||||
|  |                     const joinedFile = new File({ | ||||||
|  |                         cwd: firstFile.cwd, | ||||||
|  |                         base: firstFile.base, | ||||||
|  |                         path: joinedPath, | ||||||
|  |                         contents: self.treatMergedData(data), | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |                     this.emit('data', joinedFile); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 this.emit('end'); | ||||||
|  |             })) | ||||||
|  |             .pipe(gulp.dest(pathLib.join('./src/assets', 'lang'))) | ||||||
|  |             .on('end', done); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Treats a file to merge JSONs. This function is based on gulp-jsoncombine module. | ||||||
|  |      * https://github.com/reflog/gulp-jsoncombine
 | ||||||
|  |      * | ||||||
|  |      * @param file File treated. | ||||||
|  |      * @param data Object where to store the data. | ||||||
|  |      */ | ||||||
|  |     treatFile(file, data) { | ||||||
|  |         if (file.isNull() || file.isStream()) { | ||||||
|  |             return; // ignore
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             let srcPos = file.path.lastIndexOf('/src/'); | ||||||
|  |             if (srcPos == -1) { | ||||||
|  |                 // It's probably a Windows environment.
 | ||||||
|  |                 srcPos = file.path.lastIndexOf('\\src\\'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const path = file.path.substr(srcPos + 5); | ||||||
|  |             data[path] = JSON.parse(file.contents.toString()); | ||||||
|  |         } catch (err) { | ||||||
|  |             console.log('Error parsing JSON: ' + err); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Treats the merged JSON data, adding prefixes depending on the component. | ||||||
|  |      * | ||||||
|  |      * @param data Merged data. | ||||||
|  |      * @return Buffer with the treated data. | ||||||
|  |      */ | ||||||
|  |     treatMergedData(data) { | ||||||
|  |         const merged = {}; | ||||||
|  |         const mergedOrdered = {}; | ||||||
|  | 
 | ||||||
|  |         for (let filepath in data) { | ||||||
|  |             const pathSplit = filepath.split(/[\/\\]/); | ||||||
|  |             let prefix; | ||||||
|  | 
 | ||||||
|  |             pathSplit.pop(); | ||||||
|  | 
 | ||||||
|  |             switch (pathSplit[0]) { | ||||||
|  |                 case 'lang': | ||||||
|  |                     prefix = 'core'; | ||||||
|  |                     break; | ||||||
|  |                 case 'core': | ||||||
|  |                     if (pathSplit[1] == 'lang') { | ||||||
|  |                         // Not used right now.
 | ||||||
|  |                         prefix = 'core'; | ||||||
|  |                     } else { | ||||||
|  |                         prefix = 'core.' + pathSplit[1]; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case 'addon': | ||||||
|  |                     // Remove final item 'lang'.
 | ||||||
|  |                     pathSplit.pop(); | ||||||
|  |                     // Remove first item 'addon'.
 | ||||||
|  |                     pathSplit.shift(); | ||||||
|  | 
 | ||||||
|  |                     // For subplugins. We'll use plugin_subfolder_subfolder2_...
 | ||||||
|  |                     // E.g. 'mod_assign_feedback_comments'.
 | ||||||
|  |                     prefix = 'addon.' + pathSplit.join('_'); | ||||||
|  |                     break; | ||||||
|  |                 case 'assets': | ||||||
|  |                     prefix = 'assets.' + pathSplit[1]; | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (prefix) { | ||||||
|  |                 this.addProperties(merged, data[filepath], prefix + '.'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Force ordering by string key.
 | ||||||
|  |         Object.keys(merged).sort().forEach((key) => { | ||||||
|  |             mergedOrdered[key] = merged[key]; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return bufferFrom(JSON.stringify(mergedOrdered, null, 4)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = BuildLangTask; | ||||||
							
								
								
									
										164
									
								
								gulp/task-combine-scss.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								gulp/task-combine-scss.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,164 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | const gulp = require('gulp'); | ||||||
|  | const through = require('through'); | ||||||
|  | const bufferFrom = require('buffer-from'); | ||||||
|  | const concat = require('gulp-concat'); | ||||||
|  | const pathLib = require('path'); | ||||||
|  | const fs = require('fs'); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Task to combine scss into a single file. | ||||||
|  |  */ | ||||||
|  | class CombineScssTask { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Finds the file and returns its content. | ||||||
|  |      * | ||||||
|  |      * @param capture Import file path. | ||||||
|  |      * @param baseDir Directory where the file was found. | ||||||
|  |      * @param paths Alternative paths where to find the imports. | ||||||
|  |      * @param parsedFiles Already parsed files to reduce size of the result. | ||||||
|  |      * @return Partially combined scss. | ||||||
|  |      */ | ||||||
|  |     getReplace(capture, baseDir, paths, parsedFiles) { | ||||||
|  |         let parse = pathLib.parse(pathLib.resolve(baseDir, capture + '.scss')); | ||||||
|  |         let file = parse.dir + '/' + parse.name; | ||||||
|  | 
 | ||||||
|  |         if (file.slice(-3) === '.wp') { | ||||||
|  |             console.log('Windows Phone not supported "' + capture); | ||||||
|  |             // File was already parsed, leave the import commented.
 | ||||||
|  |             return '// @import "' + capture + '";'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!fs.existsSync(file + '.scss')) { | ||||||
|  |             // File not found, might be a partial file.
 | ||||||
|  |             file = parse.dir + '/_' + parse.name; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // If file still not found, try to find the file in the alternative paths.
 | ||||||
|  |         let x = 0; | ||||||
|  |         while (!fs.existsSync(file + '.scss') && paths.length > x) { | ||||||
|  |             parse = pathLib.parse(pathLib.resolve(paths[x], capture + '.scss')); | ||||||
|  |             file = parse.dir + '/' + parse.name; | ||||||
|  | 
 | ||||||
|  |             x++; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         file = file + '.scss'; | ||||||
|  | 
 | ||||||
|  |         if (!fs.existsSync(file)) { | ||||||
|  |             // File not found. Leave the import there.
 | ||||||
|  |             console.log('File "' + capture + '" not found'); | ||||||
|  |             return '@import "' + capture + '";'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (parsedFiles.indexOf(file) >= 0) { | ||||||
|  |             console.log('File "' + capture + '" already parsed'); | ||||||
|  |             // File was already parsed, leave the import commented.
 | ||||||
|  |             return '// @import "' + capture + '";'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         parsedFiles.push(file); | ||||||
|  |         const text = fs.readFileSync(file); | ||||||
|  | 
 | ||||||
|  |         // Recursive call.
 | ||||||
|  |         return this.scssCombine(text, parse.dir, paths, parsedFiles); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Run the task. | ||||||
|  |      * | ||||||
|  |      * @param done Function to call when done. | ||||||
|  |      */ | ||||||
|  |     run(done) { | ||||||
|  |         const paths = [ | ||||||
|  |             'node_modules/ionic-angular/themes/', | ||||||
|  |             'node_modules/font-awesome/scss/', | ||||||
|  |             'node_modules/ionicons/dist/scss/' | ||||||
|  |         ]; | ||||||
|  |         const parsedFiles = []; | ||||||
|  |         const self = this; | ||||||
|  | 
 | ||||||
|  |         gulp.src([ | ||||||
|  |                 './src/theme/variables.scss', | ||||||
|  |                 './node_modules/ionic-angular/themes/ionic.globals.*.scss', | ||||||
|  |                 './node_modules/ionic-angular/themes/ionic.components.scss', | ||||||
|  |                 './src/**/*.scss', | ||||||
|  |             ]).pipe(through(function(file) { // Combine them based on @import and save it to stream.
 | ||||||
|  |                 if (file.isNull()) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 parsedFiles.push(file); | ||||||
|  |                 file.contents = bufferFrom(self.scssCombine( | ||||||
|  |                         file.contents, pathLib.dirname(file.path), paths, parsedFiles)); | ||||||
|  | 
 | ||||||
|  |                 this.emit('data', file); | ||||||
|  |             })).pipe(concat('combined.scss')) // Concat the stream output in single file.
 | ||||||
|  |             .pipe(gulp.dest('.')) // Save file to destination.
 | ||||||
|  |             .on('end', done); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Combine scss files with its imports | ||||||
|  |      * | ||||||
|  |      * @param content Scss string to treat. | ||||||
|  |      * @param baseDir Directory where the file was found. | ||||||
|  |      * @param paths Alternative paths where to find the imports. | ||||||
|  |      * @param parsedFiles Already parsed files to reduce size of the result. | ||||||
|  |      * @return Scss string with the replaces done. | ||||||
|  |      */ | ||||||
|  |     scssCombine(content, baseDir, paths, parsedFiles) { | ||||||
|  |         // Content is a Buffer, convert to string.
 | ||||||
|  |         if (typeof content != "string") { | ||||||
|  |             content = content.toString(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Search of single imports.
 | ||||||
|  |         let regex = /@import[ ]*['"](.*)['"][ ]*;/g; | ||||||
|  | 
 | ||||||
|  |         if (regex.test(content)) { | ||||||
|  |             return content.replace(regex, (m, capture) => { | ||||||
|  |                 if (capture == "bmma") { | ||||||
|  |                     return m; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return this.getReplace(capture, baseDir, paths, parsedFiles); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Search of multiple imports.
 | ||||||
|  |         regex = /@import(?:[ \n]+['"](.*)['"][,]?[ \n]*)+;/gm; | ||||||
|  |         if (regex.test(content)) { | ||||||
|  |             return content.replace(regex, (m, capture) => { | ||||||
|  |                 let text = ''; | ||||||
|  | 
 | ||||||
|  |                 // Divide the import into multiple files.
 | ||||||
|  |                 const captures = m.match(/['"]([^'"]*)['"]/g); | ||||||
|  | 
 | ||||||
|  |                 for (let x in captures) { | ||||||
|  |                     text += this.getReplace(captures[x].replace(/['"]+/g, ''), baseDir, paths, parsedFiles) + '\n'; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return text; | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return content; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = CombineScssTask; | ||||||
							
								
								
									
										79
									
								
								gulp/task-copy-component-templates.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								gulp/task-copy-component-templates.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | const fs = require('fs'); | ||||||
|  | const gulp = require('gulp'); | ||||||
|  | const flatten = require('gulp-flatten'); | ||||||
|  | const htmlmin = require('gulp-htmlmin'); | ||||||
|  | const pathLib = require('path'); | ||||||
|  | 
 | ||||||
|  | const TEMPLATES_SRC = [ | ||||||
|  |     './src/components/**/*.html', | ||||||
|  |     './src/core/**/components/**/*.html', | ||||||
|  |     './src/core/**/component/**/*.html', | ||||||
|  |     // Copy all addon components because any component can be injected using extraImports.
 | ||||||
|  |     './src/addon/**/components/**/*.html', | ||||||
|  |     './src/addon/**/component/**/*.html' | ||||||
|  | ]; | ||||||
|  | const TEMPLATES_DEST = './www/templates'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Task to copy component templates to www to make compile-html work in AOT. | ||||||
|  |  */ | ||||||
|  | class CopyComponentTemplatesTask { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Delete a folder and all its contents. | ||||||
|  |      * | ||||||
|  |      * @param path [description] | ||||||
|  |      * @return {[type]}      [description] | ||||||
|  |      */ | ||||||
|  |     deleteFolderRecursive(path) { | ||||||
|  |         if (fs.existsSync(path)) { | ||||||
|  |             fs.readdirSync(path).forEach((file) => { | ||||||
|  |                 var curPath = pathLib.join(path, file); | ||||||
|  | 
 | ||||||
|  |                 if (fs.lstatSync(curPath).isDirectory()) { | ||||||
|  |                     this.deleteFolderRecursive(curPath); | ||||||
|  |                 } else { | ||||||
|  |                     fs.unlinkSync(curPath); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             fs.rmdirSync(path); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Run the task. | ||||||
|  |      * | ||||||
|  |      * @param done Callback to call once done. | ||||||
|  |      */ | ||||||
|  |     run(done) { | ||||||
|  |         this.deleteFolderRecursive(TEMPLATES_DEST); | ||||||
|  | 
 | ||||||
|  |         gulp.src(TEMPLATES_SRC, { allowEmpty: true }) | ||||||
|  |             .pipe(flatten()) | ||||||
|  |             // Check options here: https://github.com/kangax/html-minifier
 | ||||||
|  |             .pipe(htmlmin({ | ||||||
|  |                 collapseWhitespace: true, | ||||||
|  |                 removeComments: true, | ||||||
|  |                 caseSensitive: true | ||||||
|  |             })) | ||||||
|  |             .pipe(gulp.dest(TEMPLATES_DEST)) | ||||||
|  |             .on('end', done); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = CopyComponentTemplatesTask; | ||||||
							
								
								
									
										280
									
								
								gulp/task-push.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								gulp/task-push.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,280 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | const gulp = require('gulp'); | ||||||
|  | const inquirer = require('inquirer'); | ||||||
|  | const DevConfig = require('./dev-config'); | ||||||
|  | const Git = require('./git'); | ||||||
|  | const Jira = require('./jira'); | ||||||
|  | const Utils = require('./utils'); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Task to push a git branch and update tracker issue. | ||||||
|  |  */ | ||||||
|  | class PushTask { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Ask the user whether he wants to continue. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved with boolean: true if he wants to continue. | ||||||
|  |      */ | ||||||
|  |     async askConfirmContinue() { | ||||||
|  |         const answer = await inquirer.prompt([ | ||||||
|  |             { | ||||||
|  |                 type: 'input', | ||||||
|  |                 name: 'confirm', | ||||||
|  |                 message: 'Are you sure you want to continue?', | ||||||
|  |                 default: 'n', | ||||||
|  |             }, | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |         return answer.confirm == 'y'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Push a patch to the tracker and remove the previous one. | ||||||
|  |      * | ||||||
|  |      * @param branch Branch name. | ||||||
|  |      * @param branchData Parsed branch data. | ||||||
|  |      * @param remote Remote used. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async pushPatch(branch, branchData, remote) { | ||||||
|  |         const headCommit = await Git.getHeadCommit(branch, branchData); | ||||||
|  | 
 | ||||||
|  |         if (!headCommit) { | ||||||
|  |             throw new Error('Head commit not resolved, abort pushing patch.'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Create the patch file.
 | ||||||
|  |         const fileName = branch + '.patch'; | ||||||
|  |         const tmpPatchPath = `./tmp/${fileName}`; | ||||||
|  | 
 | ||||||
|  |         await Git.createPatch(`${headCommit}...${branch}`, tmpPatchPath); | ||||||
|  |         console.log('Git patch created'); | ||||||
|  | 
 | ||||||
|  |         // Check if there is an attachment with same name in the issue.
 | ||||||
|  |         const issue = await Jira.getIssue(branchData.issue, 'attachment'); | ||||||
|  | 
 | ||||||
|  |         let existingAttachmentId; | ||||||
|  |         const attachments = (issue.fields && issue.fields.attachment) || []; | ||||||
|  |         for (const i in attachments) { | ||||||
|  |             if (attachments[i].filename == fileName) { | ||||||
|  |                 // Found an existing attachment with the same name, we keep track of it.
 | ||||||
|  |                 existingAttachmentId = attachments[i].id; | ||||||
|  |                 break | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Push the patch to the tracker.
 | ||||||
|  |         console.log(`Uploading patch ${fileName} to the tracker...`); | ||||||
|  |         await Jira.upload(branchData.issue, tmpPatchPath); | ||||||
|  | 
 | ||||||
|  |         if (existingAttachmentId) { | ||||||
|  |             // On success, deleting file that was there before.
 | ||||||
|  |             try { | ||||||
|  |                 console.log('Deleting older patch...') | ||||||
|  |                 await Jira.deleteAttachment(existingAttachmentId); | ||||||
|  |             } catch (error) { | ||||||
|  |                 console.log('Could not delete older attachment.'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Run the task. | ||||||
|  |      * | ||||||
|  |      * @param args Command line arguments. | ||||||
|  |      * @param done Function to call when done. | ||||||
|  |      */ | ||||||
|  |     async run(args, done) { | ||||||
|  |         try { | ||||||
|  |             const remote = args.remote || DevConfig.get('upstreamRemote', 'origin'); | ||||||
|  |             let branch = args.branch; | ||||||
|  |             const force = !!args.force; | ||||||
|  | 
 | ||||||
|  |             if (!branch) { | ||||||
|  |                 branch = await Git.getCurrentBranch(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!branch) { | ||||||
|  |                 throw new Error('Cannot determine the current branch. Please make sure youu aren\'t in detached HEAD state'); | ||||||
|  |             } else if (branch == 'HEAD') { | ||||||
|  |                 throw new Error('Cannot push HEAD branch'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Parse the branch to get the project and issue number.
 | ||||||
|  |             const branchData = Utils.parseBranch(branch); | ||||||
|  |             const keepRunning = await this.validateCommitMessages(branchData); | ||||||
|  | 
 | ||||||
|  |             if (!keepRunning) { | ||||||
|  |                 // Last commit not valid, stop.
 | ||||||
|  |                 console.log('Exiting...'); | ||||||
|  |                 done(); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!args.patch) { | ||||||
|  |                 // Check if it's a security issue to force patch mode.
 | ||||||
|  |                 try { | ||||||
|  |                     args.patch = await Jira.isSecurityIssue(branchData.issue); | ||||||
|  | 
 | ||||||
|  |                     if (args.patch) { | ||||||
|  |                         console.log(`${branchData.issue} appears to be a security issue, switching to patch mode...`); | ||||||
|  |                     } | ||||||
|  |                 } catch (error) { | ||||||
|  |                     console.log(`Could not check if ${branchData.issue} is a security issue.`); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (args.patch) { | ||||||
|  |                 // Create and upload a patch file.
 | ||||||
|  |                 await this.pushPatch(branch, branchData, remote); | ||||||
|  |             } else { | ||||||
|  |                 // Push the branch.
 | ||||||
|  |                 console.log(`Pushing branch ${branch} to remote ${remote}...`); | ||||||
|  |                 await Git.push(remote, branch, force); | ||||||
|  | 
 | ||||||
|  |                 // Update tracker info.
 | ||||||
|  |                 console.log(`Branch pushed, update tracker info...`); | ||||||
|  |                 await this.updateTrackerGitInfo(branch, branchData, remote); | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error(error); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         done(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Update git info in the tracker issue. | ||||||
|  |      * | ||||||
|  |      * @param branch Branch name. | ||||||
|  |      * @param branchData Parsed branch data. | ||||||
|  |      * @param remote Remote used. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async updateTrackerGitInfo(branch, branchData, remote) { | ||||||
|  |         // Get the repository data for the project.
 | ||||||
|  |         let repositoryUrl = DevConfig.get(branchData.project + '.repositoryUrl'); | ||||||
|  |         let diffUrlTemplate = DevConfig.get(branchData.project + '.diffUrlTemplate', ''); | ||||||
|  |         let remoteUrl; | ||||||
|  | 
 | ||||||
|  |         if (!repositoryUrl) { | ||||||
|  |             // Calculate the repositoryUrl based on the remote URL.
 | ||||||
|  |             remoteUrl = await Git.getRemoteUrl(remote); | ||||||
|  | 
 | ||||||
|  |             repositoryUrl = remoteUrl.replace(/^https?:\/\//, 'git://'); | ||||||
|  |             if (!repositoryUrl.match(/\.git$/)) { | ||||||
|  |                 repositoryUrl += '.git'; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!diffUrlTemplate) { | ||||||
|  |             // Calculate the diffUrlTemplate based on the remote URL.
 | ||||||
|  |             if (!remoteUrl) { | ||||||
|  |                 remoteUrl = await Git.getRemoteUrl(remoteUrl); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             diffUrlTemplate = remoteUrl + '/compare/%headcommit%...%branch%'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Search HEAD commit to put in the diff URL.
 | ||||||
|  |         console.log ('Searching for head commit...'); | ||||||
|  |         let headCommit = await Git.getHeadCommit(branch, branchData); | ||||||
|  | 
 | ||||||
|  |         if (!headCommit) { | ||||||
|  |             throw new Error('Head commit not resolved, aborting update of tracker fields'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         headCommit = headCommit.substr(0, 10); | ||||||
|  |         console.log(`Head commit resolved to ${headCommit}`); | ||||||
|  | 
 | ||||||
|  |         // Calculate last properties needed.
 | ||||||
|  |         const diffUrl = diffUrlTemplate.replace('%branch%', branch).replace('%headcommit%', headCommit); | ||||||
|  |         const fieldRepositoryUrl = DevConfig.get('tracker.fieldnames.repositoryurl', 'Pull  from Repository'); | ||||||
|  |         const fieldBranch = DevConfig.get('tracker.fieldnames.branch', 'Pull Master Branch'); | ||||||
|  |         const fieldDiffUrl = DevConfig.get('tracker.fieldnames.diffurl', 'Pull Master Diff URL'); | ||||||
|  | 
 | ||||||
|  |         // Update tracker fields.
 | ||||||
|  |         const updates = {}; | ||||||
|  |         updates[fieldRepositoryUrl] = repositoryUrl; | ||||||
|  |         updates[fieldBranch] = branch; | ||||||
|  |         updates[fieldDiffUrl] = diffUrl; | ||||||
|  | 
 | ||||||
|  |         console.log('Setting tracker fields...'); | ||||||
|  |         await Jira.setCustomFields(branchData.issue, updates); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Validate commit messages comparing them with the branch name. | ||||||
|  |      * | ||||||
|  |      * @param branchData Parsed branch data. | ||||||
|  |      * @return True if value is ok or the user wants to continue anyway, false to stop. | ||||||
|  |      */ | ||||||
|  |     async validateCommitMessages(branchData) { | ||||||
|  |         const messages = await Git.messages(30); | ||||||
|  | 
 | ||||||
|  |         let numConsecutive = 0; | ||||||
|  |         let wrongCommitCandidate = null; | ||||||
|  | 
 | ||||||
|  |         for (let i = 0; i < messages.length; i++) { | ||||||
|  |             const message = messages[i]; | ||||||
|  |             const issue = Utils.getIssueFromCommitMessage(message); | ||||||
|  | 
 | ||||||
|  |             if (!issue || issue != branchData.issue) { | ||||||
|  |                 if (i === 0) { | ||||||
|  |                     // Last commit is wrong, it shouldn't happen. Ask the user if he wants to continue.
 | ||||||
|  |                     if (!issue) { | ||||||
|  |                         console.log('The issue number could not be found in the last commit message.'); | ||||||
|  |                         console.log(`Commit: ${message}`); | ||||||
|  |                     } else if (issue != branchData.issue) { | ||||||
|  |                         console.log('The issue number in the last commit does not match the branch being pushed to.'); | ||||||
|  |                         console.log(`Branch: ${branchData.issue} vs. commit: ${issue}`); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     return this.askConfirmContinue(); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 numConsecutive++; | ||||||
|  |                 if (numConsecutive > 2) { | ||||||
|  |                     // 3 consecutive commits with different branch, probably the branch commits are over. Everything OK.
 | ||||||
|  |                     return true; | ||||||
|  |                 } else if (!wrongCommitCandidate) { | ||||||
|  |                     wrongCommitCandidate = { | ||||||
|  |                         message: message, | ||||||
|  |                         issue: issue, | ||||||
|  |                         index: i, | ||||||
|  |                     }; | ||||||
|  |                 } | ||||||
|  |             } else if (wrongCommitCandidate) { | ||||||
|  |                 // We've found a commit with the branch name after a commit with a different branch. Probably wrong commit.
 | ||||||
|  |                 if (!wrongCommitCandidate.issue) { | ||||||
|  |                     console.log('The issue number could not be found in one of the commit messages.'); | ||||||
|  |                     console.log(`Commit: ${wrongCommitCandidate.message}`); | ||||||
|  |                 } else { | ||||||
|  |                     console.log('The issue number in a certain commit does not match the branch being pushed to.'); | ||||||
|  |                     console.log(`Branch: ${branchData.issue} vs. commit: ${wrongCommitCandidate.issue}`); | ||||||
|  |                     console.log(`Commit message: ${wrongCommitCandidate.message}`); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return this.askConfirmContinue(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = PushTask; | ||||||
							
								
								
									
										79
									
								
								gulp/url.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								gulp/url.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Class with helper functions for urls. | ||||||
|  |  */ | ||||||
|  | class Url { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add params to a URL. | ||||||
|  |      * | ||||||
|  |      * @param url URL to add the params to. | ||||||
|  |      * @param params Object with the params to add. | ||||||
|  |      * @return URL with params. | ||||||
|  |      */ | ||||||
|  |     static addParamsToUrl(url, params) { | ||||||
|  |         let separator = url.indexOf('?') != -1 ? '&' : '?'; | ||||||
|  | 
 | ||||||
|  |         for (const key in params) { | ||||||
|  |             let value = params[key]; | ||||||
|  | 
 | ||||||
|  |             // Ignore objects.
 | ||||||
|  |             if (typeof value != 'object') { | ||||||
|  |                 url += separator + key + '=' + value; | ||||||
|  |                 separator = '&'; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return url; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Parse parts of a url, using an implicit protocol if it is missing from the url. | ||||||
|  |      * | ||||||
|  |      * @param url Url. | ||||||
|  |      * @return Url parts. | ||||||
|  |      */ | ||||||
|  |     static parse(url) { | ||||||
|  |         // Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B.
 | ||||||
|  |         const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); | ||||||
|  | 
 | ||||||
|  |         if (!match) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const host = match[4] || ''; | ||||||
|  | 
 | ||||||
|  |         // Get the credentials and the port from the host.
 | ||||||
|  |         const [domainAndPort, credentials] = host.split('@').reverse(); | ||||||
|  |         const [domain, port] = domainAndPort.split(':'); | ||||||
|  |         const [username, password] = credentials ? credentials.split(':') : []; | ||||||
|  | 
 | ||||||
|  |         // Prepare parts replacing empty strings with undefined.
 | ||||||
|  |         return { | ||||||
|  |             protocol: match[2] || undefined, | ||||||
|  |             domain: domain || undefined, | ||||||
|  |             port: port || undefined, | ||||||
|  |             credentials: credentials || undefined, | ||||||
|  |             username: username || undefined, | ||||||
|  |             password: password || undefined, | ||||||
|  |             path: match[5] || undefined, | ||||||
|  |             query: match[7] || undefined, | ||||||
|  |             fragment: match[9] || undefined, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = Url; | ||||||
							
								
								
									
										119
									
								
								gulp/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								gulp/utils.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | const DevConfig = require('./dev-config'); | ||||||
|  | const DEFAULT_ISSUE_REGEX = '(MOBILE)[-_]([0-9]+)'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Class with some utility functions. | ||||||
|  |  */ | ||||||
|  | class Utils { | ||||||
|  |     /** | ||||||
|  |      * Concatenate several paths, adding a slash between them if needed. | ||||||
|  |      * | ||||||
|  |      * @param paths List of paths. | ||||||
|  |      * @return Concatenated path. | ||||||
|  |      */ | ||||||
|  |     static concatenatePaths(paths) { | ||||||
|  |         if (!paths.length) { | ||||||
|  |             return ''; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Remove all slashes between paths.
 | ||||||
|  |         for (let i = 0; i < paths.length; i++) { | ||||||
|  |             if (!paths[i]) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (i === 0) { | ||||||
|  |                 paths[i] = String(paths[i]).replace(/\/+$/g, ''); | ||||||
|  |             } else if (i === paths.length - 1) { | ||||||
|  |                 paths[i] = String(paths[i]).replace(/^\/+/g, ''); | ||||||
|  |             } else { | ||||||
|  |                 paths[i] = String(paths[i]).replace(/^\/+|\/+$/g, ''); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Remove empty paths.
 | ||||||
|  |         paths = paths.filter(path => !!path); | ||||||
|  | 
 | ||||||
|  |         return paths.join('/'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get command line arguments. | ||||||
|  |      * | ||||||
|  |      * @return Object with command line arguments. | ||||||
|  |      */ | ||||||
|  |     static getCommandLineArguments() { | ||||||
|  | 
 | ||||||
|  |         let args = {}, opt, thisOpt, curOpt; | ||||||
|  |         for (let a = 0; a < process.argv.length; a++) { | ||||||
|  | 
 | ||||||
|  |             thisOpt = process.argv[a].trim(); | ||||||
|  |             opt = thisOpt.replace(/^\-+/, ''); | ||||||
|  | 
 | ||||||
|  |             if (opt === thisOpt) { | ||||||
|  |                 // argument value
 | ||||||
|  |                 if (curOpt) { | ||||||
|  |                     args[curOpt] = opt; | ||||||
|  |                 } | ||||||
|  |                 curOpt = null; | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 // Argument name.
 | ||||||
|  |                 curOpt = opt; | ||||||
|  |                 args[curOpt] = true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return args; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Given a commit message, return the issue name (e.g. MOBILE-1234). | ||||||
|  |      * | ||||||
|  |      * @param commit Commit message. | ||||||
|  |      * @return Issue name. | ||||||
|  |      */ | ||||||
|  |     static getIssueFromCommitMessage(commit) { | ||||||
|  |         const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); | ||||||
|  |         const matches = commit.match(regex); | ||||||
|  | 
 | ||||||
|  |         return matches && matches[0]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Parse a branch name to extract some data. | ||||||
|  |      * | ||||||
|  |      * @param branch Branch name to parse. | ||||||
|  |      * @return Data. | ||||||
|  |      */ | ||||||
|  |     static parseBranch(branch) { | ||||||
|  |         const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); | ||||||
|  | 
 | ||||||
|  |         const matches = branch.match(regex); | ||||||
|  |         if (!matches || matches.length < 3) { | ||||||
|  |             throw new Error(`Error parsing branch ${branch}`); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             issue: matches[0], | ||||||
|  |             project: matches[1], | ||||||
|  |             issueNumber: matches[2], | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = Utils; | ||||||
							
								
								
									
										457
									
								
								gulpfile.js
									
									
									
									
									
								
							
							
						
						
									
										457
									
								
								gulpfile.js
									
									
									
									
									
								
							| @ -1,193 +1,27 @@ | |||||||
| var gulp = require('gulp'), | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|     fs = require('fs'), | //
 | ||||||
|     through = require('through'), | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|     rename = require('gulp-rename'), | // you may not use this file except in compliance with the License.
 | ||||||
|     path = require('path'), | // You may obtain a copy of the License at
 | ||||||
|     slash = require('gulp-slash'), | //
 | ||||||
|     clipEmptyFiles = require('gulp-clip-empty-files'), | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|     File = require('vinyl'), | //
 | ||||||
|     flatten = require('gulp-flatten'), | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|     npmPath = require('path'), | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|     concat = require('gulp-concat'), | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|     htmlmin = require('gulp-htmlmin'), | // See the License for the specific language governing permissions and
 | ||||||
|     bufferFrom = require('buffer-from'), | // limitations under the License.
 | ||||||
|     exec = require('child_process').exec, |  | ||||||
|     license = '' + |  | ||||||
|         '// (C) Copyright 2015 Moodle Pty Ltd.\n' + |  | ||||||
|         '//\n' + |  | ||||||
|         '// Licensed under the Apache License, Version 2.0 (the "License");\n' + |  | ||||||
|         '// you may not use this file except in compliance with the License.\n' + |  | ||||||
|         '// You may obtain a copy of the License at\n' + |  | ||||||
|         '//\n' + |  | ||||||
|         '//     http://www.apache.org/licenses/LICENSE-2.0\n' + |  | ||||||
|         '//\n' + |  | ||||||
|         '// Unless required by applicable law or agreed to in writing, software\n' + |  | ||||||
|         '// distributed under the License is distributed on an "AS IS" BASIS,\n' + |  | ||||||
|         '// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' + |  | ||||||
|         '// See the License for the specific language governing permissions and\n' + |  | ||||||
|         '// limitations under the License.\n\n'; |  | ||||||
| 
 | 
 | ||||||
| /** | const BuildConfigTask = require('./gulp/task-build-config'); | ||||||
|  * Copy a property from one object to another, adding a prefix to the key if needed. | const BuildLangTask = require('./gulp/task-build-lang'); | ||||||
|  * @param {Object} target Object to copy the properties to. | const CombineScssTask = require('./gulp/task-combine-scss'); | ||||||
|  * @param {Object} source Object to copy the properties from. | const CopyComponentTemplatesTask = require('./gulp/task-copy-component-templates'); | ||||||
|  * @param {String} prefix Prefix to add to the keys. | const PushTask = require('./gulp/task-push'); | ||||||
|  */ | const Utils = require('./gulp/utils'); | ||||||
| function addProperties(target, source, prefix) { | const gulp = require('gulp'); | ||||||
|     for (var property in source) { | const pathLib = require('path'); | ||||||
|         target[prefix + property] = source[property]; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| /** | const paths = { | ||||||
|  * Treats a file to merge JSONs. This function is based on gulp-jsoncombine module. |  | ||||||
|  * https://github.com/reflog/gulp-jsoncombine
 |  | ||||||
|  * @param  {Object} file File treated. |  | ||||||
|  */ |  | ||||||
| function treatFile(file, data) { |  | ||||||
|     if (file.isNull() || file.isStream()) { |  | ||||||
|         return; // ignore
 |  | ||||||
|     } |  | ||||||
|     try { |  | ||||||
|         var srcPos = file.path.lastIndexOf('/src/'); |  | ||||||
|         if (srcPos == -1) { |  | ||||||
|             // It's probably a Windows environment.
 |  | ||||||
|             srcPos = file.path.lastIndexOf('\\src\\'); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         var path = file.path.substr(srcPos + 5); |  | ||||||
|         data[path] = JSON.parse(file.contents.toString()); |  | ||||||
|     } catch (err) { |  | ||||||
|         console.log('Error parsing JSON: ' + err); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Treats the merged JSON data, adding prefixes depending on the component. Used in lang tasks. |  | ||||||
|  * |  | ||||||
|  * @param  {Object} data Merged data. |  | ||||||
|  * @return {Buffer}      Buffer with the treated data. |  | ||||||
|  */ |  | ||||||
| function treatMergedData(data) { |  | ||||||
|     var merged = {}; |  | ||||||
|     var mergedOrdered = {}; |  | ||||||
| 
 |  | ||||||
|     for (var filepath in data) { |  | ||||||
|         var pathSplit = filepath.split(/[\/\\]/), |  | ||||||
|             prefix; |  | ||||||
| 
 |  | ||||||
|         pathSplit.pop(); |  | ||||||
| 
 |  | ||||||
|         switch (pathSplit[0]) { |  | ||||||
|             case 'lang': |  | ||||||
|                 prefix = 'core'; |  | ||||||
|                 break; |  | ||||||
|             case 'core': |  | ||||||
|                 if (pathSplit[1] == 'lang') { |  | ||||||
|                     // Not used right now.
 |  | ||||||
|                     prefix = 'core'; |  | ||||||
|                 } else { |  | ||||||
|                     prefix = 'core.' + pathSplit[1]; |  | ||||||
|                 } |  | ||||||
|                 break; |  | ||||||
|             case 'addon': |  | ||||||
|                 // Remove final item 'lang'.
 |  | ||||||
|                 pathSplit.pop(); |  | ||||||
|                 // Remove first item 'addon'.
 |  | ||||||
|                 pathSplit.shift(); |  | ||||||
| 
 |  | ||||||
|                 // For subplugins. We'll use plugin_subfolder_subfolder2_...
 |  | ||||||
|                 // E.g. 'mod_assign_feedback_comments'.
 |  | ||||||
|                 prefix = 'addon.' + pathSplit.join('_'); |  | ||||||
|                 break; |  | ||||||
|             case 'assets': |  | ||||||
|                 prefix = 'assets.' + pathSplit[1]; |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (prefix) { |  | ||||||
|             addProperties(merged, data[filepath], prefix + '.'); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Force ordering by string key.
 |  | ||||||
|     Object.keys(merged).sort().forEach(function(k){ |  | ||||||
|         mergedOrdered[k] = merged[k]; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return bufferFrom(JSON.stringify(mergedOrdered, null, 4)); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Build lang file. |  | ||||||
|  * |  | ||||||
|  * @param  {String} language    Language to translate. |  | ||||||
|  * @param  {String[]} langPaths Paths to the possible language files. |  | ||||||
|  * @param  {String}   buildDest Path where to leave the built files. |  | ||||||
|  * @param  {Function} done      Function to call when done. |  | ||||||
|  * @return {Void} |  | ||||||
|  */ |  | ||||||
| function buildLang(language, langPaths, buildDest, done) { |  | ||||||
|     var filename = language + '.json', |  | ||||||
|         data = {}, |  | ||||||
|         firstFile = null; |  | ||||||
| 
 |  | ||||||
|     var paths = langPaths.map(function(path) { |  | ||||||
|         if (path.slice(-1) != '/') { |  | ||||||
|             path = path + '/'; |  | ||||||
|         } |  | ||||||
|         return path + language + '.json'; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     gulp.src(paths, { allowEmpty: true }) |  | ||||||
|         .pipe(slash()) |  | ||||||
|         .pipe(clipEmptyFiles()) |  | ||||||
|         .pipe(through(function(file) { |  | ||||||
|             if (!firstFile) { |  | ||||||
|                 firstFile = file; |  | ||||||
|             } |  | ||||||
|             return treatFile(file, data); |  | ||||||
|         }, function() { |  | ||||||
|             /* This implementation is based on gulp-jsoncombine module. |  | ||||||
|              * https://github.com/reflog/gulp-jsoncombine */
 |  | ||||||
|             if (firstFile) { |  | ||||||
|                 var joinedPath = path.join(firstFile.base, language+'.json'); |  | ||||||
| 
 |  | ||||||
|                 var joinedFile = new File({ |  | ||||||
|                     cwd: firstFile.cwd, |  | ||||||
|                     base: firstFile.base, |  | ||||||
|                     path: joinedPath, |  | ||||||
|                     contents: treatMergedData(data) |  | ||||||
|                 }); |  | ||||||
| 
 |  | ||||||
|                 this.emit('data', joinedFile); |  | ||||||
|             } |  | ||||||
|             this.emit('end'); |  | ||||||
|         })) |  | ||||||
|         .pipe(gulp.dest(buildDest)) |  | ||||||
|         .on('end', done); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Delete a folder and all its contents.
 |  | ||||||
| function deleteFolderRecursive(path) { |  | ||||||
|   if (fs.existsSync(path)) { |  | ||||||
|     fs.readdirSync(path).forEach(function(file) { |  | ||||||
|       var curPath = npmPath.join(path, file); |  | ||||||
|       if (fs.lstatSync(curPath).isDirectory()) { |  | ||||||
|         deleteFolderRecursive(curPath); |  | ||||||
|       } else { |  | ||||||
|         fs.unlinkSync(curPath); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     fs.rmdirSync(path); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // List of app lang files. To be used only if cannot get it from filesystem.
 |  | ||||||
| var paths = { |  | ||||||
|         src: './src', |  | ||||||
|         assets: './src/assets', |  | ||||||
|     lang: [ |     lang: [ | ||||||
|         './src/lang/', |         './src/lang/', | ||||||
|         './src/core/**/lang/', |         './src/core/**/lang/', | ||||||
| @ -198,240 +32,37 @@ var paths = { | |||||||
|     config: './src/config.json', |     config: './src/config.json', | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const args = Utils.getCommandLineArguments(); | ||||||
|  | 
 | ||||||
| // Build the language files into a single file per language.
 | // Build the language files into a single file per language.
 | ||||||
| gulp.task('lang', function(done) { | gulp.task('lang', (done) => { | ||||||
|     buildLang('en', paths.lang, path.join(paths.assets, 'lang'), done); |     new BuildLangTask().run('en', paths.lang, done); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| // Convert config.json into a TypeScript class.
 | // Convert config.json into a TypeScript class.
 | ||||||
| gulp.task('config', function(done) { | gulp.task('config', (done) => { | ||||||
|     // Get the last commit.
 |     new BuildConfigTask().run(paths.config, done); | ||||||
|     exec('git log -1 --pretty=format:"%H"', function (err, commit, stderr) { |  | ||||||
|         if (err) { |  | ||||||
|             console.error('An error occurred while getting the last commit: ' + err); |  | ||||||
|         } else if (stderr) { |  | ||||||
|             console.error('An error occurred while getting the last commit: ' + stderr); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         gulp.src(paths.config) |  | ||||||
|             .pipe(through(function(file) { |  | ||||||
|                 // Convert the contents of the file into a TypeScript class.
 |  | ||||||
|                 // Disable the rule variable-name in the file.
 |  | ||||||
|                 var config = JSON.parse(file.contents.toString()), |  | ||||||
|                     contents = license + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n', |  | ||||||
|                     that = this; |  | ||||||
| 
 |  | ||||||
|                 for (var key in config) { |  | ||||||
|                     var value = config[key]; |  | ||||||
|                     if (typeof value == 'string') { |  | ||||||
|                         // Wrap the string in ' and scape them.
 |  | ||||||
|                         value = "'" + value.replace(/([^\\])'/g, "$1\\'") + "'"; |  | ||||||
|                     } else if (typeof value != 'number' && typeof value != 'boolean') { |  | ||||||
|                         // Stringify with 4 spaces of indentation, and then add 4 more spaces in each line.
 |  | ||||||
|                         value = JSON.stringify(value, null, 4).replace(/^(?:    )/gm, '        ').replace(/^(?:})/gm, '    }'); |  | ||||||
|                         // Replace " by ' in values.
 |  | ||||||
|                         value = value.replace(/: "([^"]*)"/g, ": '$1'"); |  | ||||||
| 
 |  | ||||||
|                         // Check if the keys have "-" in it.
 |  | ||||||
|                         var matches = value.match(/"([^"]*\-[^"]*)":/g); |  | ||||||
|                         if (matches) { |  | ||||||
|                             // Replace " by ' in keys. We cannot remove them because keys have chars like '-'.
 |  | ||||||
|                             value = value.replace(/"([^"]*)":/g, "'$1':"); |  | ||||||
|                         } else { |  | ||||||
|                             // Remove ' in keys.
 |  | ||||||
|                             value = value.replace(/"([^"]*)":/g, "$1:"); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         // Add type any to the key.
 |  | ||||||
|                         key = key + ': any'; |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     // If key has quotation marks, remove them.
 |  | ||||||
|                     if (key[0] == '"') { |  | ||||||
|                         key = key.substr(1, key.length - 2); |  | ||||||
|                     } |  | ||||||
|                     contents += '    static ' + key + ' = ' + value + ';\n'; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 // Add compilation info.
 |  | ||||||
|                 contents += '    static compilationtime = ' + Date.now() + ';\n'; |  | ||||||
|                 contents += '    static lastcommit = \'' + commit + '\';\n'; |  | ||||||
| 
 |  | ||||||
|                 contents += '}\n'; |  | ||||||
| 
 |  | ||||||
|                 file.contents = bufferFrom(contents); |  | ||||||
|                 this.emit('data', file); |  | ||||||
|             })) |  | ||||||
|             .pipe(rename('configconstants.ts')) |  | ||||||
|             .pipe(gulp.dest(paths.src)) |  | ||||||
|             .on('end', done); |  | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | // Copy component templates to www to make compile-html work in AOT.
 | ||||||
|  | gulp.task('copy-component-templates', (done) => { | ||||||
|  |     new CopyComponentTemplatesTask().run(done); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // Combine SCSS files.
 | ||||||
|  | gulp.task('combine-scss', (done) => { | ||||||
|  |     new CombineScssTask().run(done); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | gulp.task('push', (done) => { | ||||||
|  |     new PushTask().run(args, done); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| gulp.task('default', gulp.parallel('lang', 'config')); | gulp.task('default', gulp.parallel('lang', 'config')); | ||||||
| 
 | 
 | ||||||
| gulp.task('watch', function() { | gulp.task('watch', () => { | ||||||
|     var langsPaths = paths.lang.map(function(path) { |     const langsPaths = paths.lang.map(path => path + 'en.json'); | ||||||
|         return path + 'en.json'; | 
 | ||||||
|     }); |  | ||||||
|     gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang')); |     gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang')); | ||||||
|     gulp.watch(paths.config, { interval: 500 }, gulp.parallel('config')); |     gulp.watch(paths.config, { interval: 500 }, gulp.parallel('config')); | ||||||
| }); | }); | ||||||
| 
 |  | ||||||
| var templatesSrc = [ |  | ||||||
|         './src/components/**/*.html', |  | ||||||
|         './src/core/**/components/**/*.html', |  | ||||||
|         './src/core/**/component/**/*.html', |  | ||||||
|         // Copy all addon components because any component can be injected using extraImports.
 |  | ||||||
|         './src/addon/**/components/**/*.html', |  | ||||||
|         './src/addon/**/component/**/*.html' |  | ||||||
|     ], |  | ||||||
|     templatesDest = './www/templates'; |  | ||||||
| 
 |  | ||||||
| // Copy component templates to www to make compile-html work in AOT.
 |  | ||||||
| gulp.task('copy-component-templates', function(done) { |  | ||||||
|     deleteFolderRecursive(templatesDest); |  | ||||||
| 
 |  | ||||||
|     gulp.src(templatesSrc, { allowEmpty: true }) |  | ||||||
|         .pipe(flatten()) |  | ||||||
|         // Check options here: https://github.com/kangax/html-minifier
 |  | ||||||
|         .pipe(htmlmin({ |  | ||||||
|           collapseWhitespace: true, |  | ||||||
|           removeComments: true, |  | ||||||
|           caseSensitive: true |  | ||||||
|         })) |  | ||||||
|         .pipe(gulp.dest(templatesDest)) |  | ||||||
|         .on('end', done); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Finds the file and returns its content. |  | ||||||
|  * |  | ||||||
|  * @param  {string} capture     Import file path. |  | ||||||
|  * @param  {string} baseDir     Directory where the file was found. |  | ||||||
|  * @param  {string} paths       Alternative paths where to find the imports. |  | ||||||
|  * @param  {Array} parsedFiles  Yet parsed files to reduce size of the result. |  | ||||||
|  * @return {string}             Partially combined scss. |  | ||||||
|  */ |  | ||||||
| function getReplace(capture, baseDir, paths, parsedFiles) { |  | ||||||
|     var parse   = path.parse(path.resolve(baseDir, capture + '.scss')); |  | ||||||
|     var file    = parse.dir + '/' + parse.name; |  | ||||||
| 
 |  | ||||||
|     if (file.slice(-3) === '.wp') { |  | ||||||
|         console.log('Windows Phone not supported "' + capture); |  | ||||||
|         // File was already parsed, leave the import commented.
 |  | ||||||
|         return '// @import "' + capture + '";'; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!fs.existsSync(file + '.scss')) { |  | ||||||
|         // File not found, might be a partial file.
 |  | ||||||
|         file    = parse.dir + '/_' + parse.name; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // If file still not found, try to find the file in the alternative paths.
 |  | ||||||
|     var x = 0; |  | ||||||
|     while (!fs.existsSync(file + '.scss') && paths.length > x) { |  | ||||||
|         parse   = path.parse(path.resolve(paths[x], capture + '.scss')); |  | ||||||
|         file    = parse.dir + '/' + parse.name; |  | ||||||
| 
 |  | ||||||
|         x++; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     file    = file + '.scss'; |  | ||||||
| 
 |  | ||||||
|     if (!fs.existsSync(file)) { |  | ||||||
|         // File not found. Leave the import there.
 |  | ||||||
|         console.log('File "' + capture + '" not found'); |  | ||||||
|         return '@import "' + capture + '";'; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (parsedFiles.indexOf(file) >= 0) { |  | ||||||
|         console.log('File "' + capture + '" already parsed'); |  | ||||||
|         // File was already parsed, leave the import commented.
 |  | ||||||
|         return '// @import "' + capture + '";'; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     parsedFiles.push(file); |  | ||||||
|     var text = fs.readFileSync(file); |  | ||||||
| 
 |  | ||||||
|     // Recursive call.
 |  | ||||||
|     return scssCombine(text, parse.dir, paths, parsedFiles); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Combine scss files with its imports |  | ||||||
|  * |  | ||||||
|  * @param  {string} content     Scss string to read. |  | ||||||
|  * @param  {string} baseDir     Directory where the file was found. |  | ||||||
|  * @param  {string} paths       Alternative paths where to find the imports. |  | ||||||
|  * @param  {Array} parsedFiles  Yet parsed files to reduce size of the result. |  | ||||||
|  * @return {string}             Scss string with the replaces done. |  | ||||||
|  */ |  | ||||||
| function scssCombine(content, baseDir, paths, parsedFiles) { |  | ||||||
| 
 |  | ||||||
|     // Content is a Buffer, convert to string.
 |  | ||||||
|     if (typeof content != "string") { |  | ||||||
|         content = content.toString(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Search of single imports.
 |  | ||||||
|     var regex = /@import[ ]*['"](.*)['"][ ]*;/g; |  | ||||||
| 
 |  | ||||||
|     if (regex.test(content)) { |  | ||||||
|         return content.replace(regex, function(m, capture) { |  | ||||||
|             if (capture == "bmma") { |  | ||||||
|                 return m; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return getReplace(capture, baseDir, paths, parsedFiles); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Search of multiple imports.
 |  | ||||||
|     regex = /@import(?:[ \n]+['"](.*)['"][,]?[ \n]*)+;/gm; |  | ||||||
|     if (regex.test(content)) { |  | ||||||
|         return content.replace(regex, function(m, capture) { |  | ||||||
|             var text = ""; |  | ||||||
| 
 |  | ||||||
|             // Divide the import into multiple files.
 |  | ||||||
|             regex = /['"]([^'"]*)['"]/g; |  | ||||||
|             var captures = m.match(regex); |  | ||||||
|             for (var x in captures) { |  | ||||||
|                 text += getReplace(captures[x].replace(/['"]+/g, ''), baseDir, paths, parsedFiles) + "\n"; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return text; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return content; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| gulp.task('combine-scss', function(done) { |  | ||||||
|     var paths = [ |  | ||||||
|         'node_modules/ionic-angular/themes/', |  | ||||||
|         'node_modules/font-awesome/scss/', |  | ||||||
|         'node_modules/ionicons/dist/scss/' |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     var parsedFiles = []; |  | ||||||
| 
 |  | ||||||
|     gulp.src([ |  | ||||||
|             './src/theme/variables.scss', |  | ||||||
|             './node_modules/ionic-angular/themes/ionic.globals.*.scss', |  | ||||||
|             './node_modules/ionic-angular/themes/ionic.components.scss', |  | ||||||
|             './src/**/*.scss'])  // define a source files
 |  | ||||||
|         .pipe(through(function(file, encoding, callback) { |  | ||||||
|             if (file.isNull()) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             parsedFiles.push(file); |  | ||||||
|             file.contents = bufferFrom(scssCombine(file.contents, path.dirname(file.path), paths, parsedFiles)); |  | ||||||
| 
 |  | ||||||
|             this.emit('data', file); |  | ||||||
|         }))   // combine them based on @import and save it to stream
 |  | ||||||
|         .pipe(concat('combined.scss')) // concat the stream output in single file
 |  | ||||||
|         .pipe(gulp.dest('.'))  // save file to destination.
 |  | ||||||
|         .on('end', done); |  | ||||||
| }); |  | ||||||
|  | |||||||
							
								
								
									
										738
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										738
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -121,6 +121,7 @@ | |||||||
|     "cordova-support-google-services": "^1.3.2", |     "cordova-support-google-services": "^1.3.2", | ||||||
|     "es6-promise-plugin": "^4.2.2", |     "es6-promise-plugin": "^4.2.2", | ||||||
|     "font-awesome": "^4.7.0", |     "font-awesome": "^4.7.0", | ||||||
|  |     "inquirer": "^7.3.2", | ||||||
|     "ionic-angular": "3.9.9", |     "ionic-angular": "3.9.9", | ||||||
|     "ionicons": "3.0.0", |     "ionicons": "3.0.0", | ||||||
|     "jszip": "^3.1.5", |     "jszip": "^3.1.5", | ||||||
| @ -155,10 +156,12 @@ | |||||||
|     "gulp-htmlmin": "^5.0.1", |     "gulp-htmlmin": "^5.0.1", | ||||||
|     "gulp-rename": "^2.0.0", |     "gulp-rename": "^2.0.0", | ||||||
|     "gulp-slash": "^1.1.3", |     "gulp-slash": "^1.1.3", | ||||||
|  |     "keytar": "^6.0.1", | ||||||
|     "lodash.template": "^4.5.0", |     "lodash.template": "^4.5.0", | ||||||
|     "minimist": "^1.2.5", |     "minimist": "^1.2.5", | ||||||
|     "native-run": "^1.0.0", |     "native-run": "^1.0.0", | ||||||
|     "node-loader": "^0.6.0", |     "node-loader": "^0.6.0", | ||||||
|  |     "request": "^2.88.2", | ||||||
|     "through": "^2.3.8", |     "through": "^2.3.8", | ||||||
|     "typescript": "~2.6.2", |     "typescript": "~2.6.2", | ||||||
|     "vinyl": "^2.2.0", |     "vinyl": "^2.2.0", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user