// (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); 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();