mirror of
https://github.com/smallmain/cocos-enhance-kit.git
synced 2024-12-25 19:28:28 +00:00
393 lines
12 KiB
JavaScript
393 lines
12 KiB
JavaScript
'use strict';
|
||
|
||
var Path = require('path');
|
||
var Chalk = require('chalk');
|
||
var Spawn = require('child_process').spawn;
|
||
var treekill = require('tree-kill');
|
||
var Async = require('async');
|
||
|
||
function exec(cmdArgs, path, cb, options) {
|
||
var timeout = (options && options.timeout) || 600000;
|
||
var autoRetry = options && options.autoRetry;
|
||
var autoKill = !options || (options.autoKill !== false);
|
||
|
||
console.log(Chalk.yellow('git ' + cmdArgs.join(' ')), 'in', Chalk.magenta(path));
|
||
|
||
var child = Spawn('git', cmdArgs, {
|
||
cwd: path,
|
||
stdio: [0, 'pipe', 'pipe']
|
||
});
|
||
|
||
var offbranch = false;
|
||
var aborted = false;
|
||
var timerId = -1;
|
||
|
||
function retry () {
|
||
console.log(Chalk.yellow(`restart "${cmdArgs[0]}": ${Path.basename(path)}`));
|
||
exec(cmdArgs, path, cb, options); // Object.assign({}, options, { autoRetry: false })
|
||
}
|
||
|
||
function onConnectionError () {
|
||
aborted = true;
|
||
clearTimeout(timerId);
|
||
console.log(Chalk.yellow(`connection timeout/error: ${Path.basename(path)}`));
|
||
treekill(child.pid);
|
||
if (autoRetry && !offbranch) {
|
||
retry();
|
||
}
|
||
else {
|
||
// console.log('+++send callback from connection timeout: ' + Path.basename(path));
|
||
cb(null, { offbranch });
|
||
}
|
||
}
|
||
|
||
timerId = setTimeout(onConnectionError, timeout);
|
||
|
||
child.stdout.on('data', function (data) {
|
||
if (aborted) return;
|
||
|
||
var text = path + ' ' + data.toString().trim();
|
||
|
||
// git stash pop
|
||
if (text.indexOf('CONFLICT (content): Merge conflict in') !== -1) {
|
||
console.error(Chalk.red(text));
|
||
process.exit(1);
|
||
return;
|
||
}
|
||
});
|
||
child.stderr.on('data', function(data) {
|
||
if (aborted) return;
|
||
|
||
var text = path + ' ' + data.toString().trim();
|
||
|
||
// git checkout ("overwritten by checkout")
|
||
// git pull ("overwritten by merge")
|
||
if (text.includes('Your local changes to the following files would be overwritten by')) {
|
||
if (!autoKill) {
|
||
console.log(Chalk.yellow(text));
|
||
clearTimeout(timerId);
|
||
aborted = true;
|
||
return cb(new Error(text));
|
||
}
|
||
}
|
||
|
||
// git pull ("error: cannot lock ref '...': ... (unable to update local ref)")
|
||
if (text.includes('error: cannot lock ref')) {
|
||
console.log(Chalk.yellow(text));
|
||
aborted = true;
|
||
setTimeout(retry, 500);
|
||
return;
|
||
}
|
||
|
||
if (text.includes('Aborting') || text.includes('fatal')) {
|
||
if (
|
||
text.indexOf('Invalid refspec') === -1 &&
|
||
text.indexOf('Couldn\'t find remote ref') === -1 &&
|
||
text.indexOf('remote fireball already exists') === -1
|
||
) {
|
||
if (text.includes('Could not read from remote repository') ||
|
||
text.includes('The remote end hung up unexpectedly')
|
||
) {
|
||
console.log(Chalk.yellow(text));
|
||
onConnectionError();
|
||
}
|
||
else {
|
||
console.error(Chalk.red(text));
|
||
process.exit(1);
|
||
}
|
||
return;
|
||
}
|
||
|
||
offbranch = true;
|
||
}
|
||
|
||
// normal message, not error
|
||
console.log(text);
|
||
});
|
||
if (cb) {
|
||
child.on('close', function (code, signal) {
|
||
if (aborted) return;
|
||
// console.log(`====closing process: ${Path.basename(path)}, code: ${code}, signal: ${signal}`);
|
||
clearTimeout(timerId);
|
||
// console.log('+++send callback from close event: ' + Path.basename(path));
|
||
cb (null, { offbranch });
|
||
});
|
||
}
|
||
}
|
||
|
||
function commit( repo, message, cb ) {
|
||
exec(['commit', '-m', message], repo, cb);
|
||
}
|
||
|
||
function checkout( repo, branch, fetch, cb ) {
|
||
var Async = require('async');
|
||
Async.series([
|
||
function ( next ) {
|
||
if (!fetch) return next();
|
||
exec(['fetch', '--all'], repo, next );
|
||
},
|
||
|
||
function ( next ) {
|
||
exec(['checkout', branch], repo, next);
|
||
},
|
||
], function ( err ) {
|
||
if ( err ) {
|
||
console.error(Chalk.red('Failed to checkout ' + repo + '. Message: ' + err.message ));
|
||
if (cb) cb (err);
|
||
return;
|
||
}
|
||
if (cb) cb();
|
||
});
|
||
}
|
||
|
||
function fetch( branch, repo, callback) {
|
||
exec(['fetch', 'fireball', branch], repo, callback, { autoRetry: true, timeout: 30000 } );
|
||
}
|
||
|
||
function checkoutTrack( branch, repo, callback) {
|
||
exec(['checkout', '-B', branch, '--track', 'fireball/' + branch], repo, callback, { autoRetry: true, timeout: 30000, autoKill: false } );
|
||
}
|
||
|
||
// 检查 git 的错误信息,看看是否可以通过调用指定的重置方法进行文件重置操作
|
||
function resetFiles (repo, log, resetFunctions) {
|
||
var fileListRE = /overwritten by \w+:\s*\n((?:\s+.*\n)*\s+.*)/;
|
||
var matches = fileListRE.exec(log);
|
||
if (matches) {
|
||
// pending changes detected
|
||
var filesToReset = matches[1].split(/\r\n|\r|\n/).map(x => x.trimLeft()).filter(x => resetFunctions[x]);
|
||
if (filesToReset.length > 0) {
|
||
// auto reset
|
||
var postProcesses = [];
|
||
for (var i = 0; i < filesToReset.length; ++i) {
|
||
var filename = filesToReset[i];
|
||
var postProcess = resetFunctions[filename](repo, Path.join(repo, filename));
|
||
if (postProcess) {
|
||
postProcesses.push(postProcess);
|
||
}
|
||
}
|
||
|
||
var postProcessAll = postProcesses.length > 0 ? function () {
|
||
for (var i = postProcesses.length - 1; i >= 0; --i) {
|
||
postProcesses[i]();
|
||
}
|
||
} : (function () {});
|
||
|
||
return { changed: true, postProcess: postProcessAll };
|
||
}
|
||
}
|
||
|
||
return { changed: false };
|
||
}
|
||
|
||
// autoResetFiles - 可选字典,用于指定要自动重置的文件。key 是文件名,例如 "package.json"
|
||
// value 是重置用的回调函数。回调函数调用时会传入要重置的文件路径以及重置后的回调函数。
|
||
function checkoutRemoteBranch (repo, branch, autoResetFiles, cb) {
|
||
if (typeof autoResetFiles === 'function') {
|
||
cb = autoResetFiles;
|
||
autoResetFiles = null;
|
||
}
|
||
Async.series([
|
||
function ( next ) {
|
||
fetch( branch, repo, next);
|
||
},
|
||
|
||
function ( next ) {
|
||
checkoutTrack(branch, repo, autoResetFiles ? function (err) {
|
||
if (!err) {
|
||
return next();
|
||
}
|
||
var res = resetFiles(repo, err.message, autoResetFiles);
|
||
if (res.changed) {
|
||
// retry after reset
|
||
checkoutTrack(branch, repo, (err) => {
|
||
res.postProcess(err);
|
||
next(err);
|
||
});
|
||
}
|
||
else {
|
||
next(err);
|
||
}
|
||
} : next);
|
||
}
|
||
], function ( err ) {
|
||
if ( err ) {
|
||
console.error(Chalk.red('Failed to checkout ' + repo + '. Message: ' + err.message ));
|
||
if (cb) cb (err);
|
||
return;
|
||
}
|
||
if (cb) cb();
|
||
});
|
||
}
|
||
|
||
function pull (repo, remote, branch, autoResetFiles, cb) {
|
||
if (!cb) {
|
||
cb = autoResetFiles;
|
||
autoResetFiles = null;
|
||
}
|
||
exec(['pull', remote, branch], repo, function (err, result) {
|
||
if (result && result.offbranch) {
|
||
console.log(Chalk.red(`Skip repos that has custom local branch checkout: "${repo}"`));
|
||
}
|
||
else if (err && autoResetFiles) {
|
||
var res = resetFiles(repo, err.message, autoResetFiles);
|
||
if (res.changed) {
|
||
// retry after reset
|
||
pull(repo, remote, branch, (err, result) => {
|
||
res.postProcess(err, result);
|
||
cb(err, result);
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
cb(err);
|
||
}, { autoRetry: true, timeout: 50000, autoKill: !autoResetFiles });
|
||
}
|
||
|
||
function push( repo, remote, branch, cb ) {
|
||
var Async = require('async');
|
||
Async.series([
|
||
function ( next ) {
|
||
exec(['push', remote, branch], repo, next );
|
||
},
|
||
|
||
function ( next ) {
|
||
exec(['push', remote, '--tags'], repo, next );
|
||
},
|
||
], function ( err ) {
|
||
if ( err ) {
|
||
console.error(Chalk.red('Failed to push ' + repo + '. Message: ' + err.message ));
|
||
if (cb) cb (err);
|
||
return;
|
||
}
|
||
if (cb) cb ();
|
||
});
|
||
}
|
||
|
||
function getCurrentBranch( path ) {
|
||
var spawnSync = require('child_process').spawnSync;
|
||
var output = spawnSync('git', ['symbolic-ref', '--short', '-q', 'HEAD'], {
|
||
cwd: path,
|
||
});
|
||
// console.log(output);
|
||
return output.stdout.toString().trim().replace(/^heads\//, '');
|
||
}
|
||
|
||
function getCurrentCommit( path, cb ) {
|
||
var child = Spawn('git', ['rev-parse', 'HEAD'], {
|
||
cwd: path,
|
||
});
|
||
var commit, err;
|
||
child.stdout.on('data', function(data) {
|
||
commit = data.toString().trim();
|
||
});
|
||
child.stderr.on('data', function(data) {
|
||
err = data.toString();
|
||
});
|
||
child.on('close', function() {
|
||
cb(err, commit);
|
||
});
|
||
// console.log(output);
|
||
// return output.stdout.toString().trim();
|
||
}
|
||
|
||
function reportStatus( path, cb ) {
|
||
var output = '';
|
||
var Async = require('async');
|
||
Async.series([
|
||
function( next ) {
|
||
var statusOut = Spawn('git', ['status', '-s'], {
|
||
cwd: path,
|
||
});
|
||
statusOut.stdout.on('data', function(data) {
|
||
output += Chalk.yellow(data.toString());
|
||
});
|
||
statusOut.on('close', function() {
|
||
next();
|
||
});
|
||
},
|
||
// function( next ) {
|
||
// var cherryOut = Spawn('git', ['cherry', '-v'], {
|
||
// cwd: path,
|
||
// });
|
||
// cherryOut.stdout.on('data', function(data) {
|
||
// output += data.toString();
|
||
// });
|
||
// cherryOut.on('close', function() {
|
||
// next();
|
||
// });
|
||
// }
|
||
], function ( err ) {
|
||
if ( err ) {
|
||
console.error(Chalk.red('Failed to report status in ' + path + '. Message: ' + err.message ));
|
||
if (cb) cb (err);
|
||
return;
|
||
}
|
||
if (cb) cb (null, output);
|
||
});
|
||
}
|
||
|
||
function parseRepo( category, name ) {
|
||
var orgName = '';
|
||
var branch = '';
|
||
var localFolder = '';
|
||
|
||
switch(category) {
|
||
case 'fireball':
|
||
return {
|
||
name: 'fireball',
|
||
branch: '',
|
||
localPath: '.',
|
||
url: ''
|
||
};
|
||
case 'builtin':
|
||
orgName = 'cocos-creator-packages';
|
||
localFolder = 'builtin';
|
||
break;
|
||
case 'hosts':
|
||
orgName = 'cocos-creator';
|
||
break;
|
||
}
|
||
|
||
var nameList = name.split('/');
|
||
if (nameList.length === 2) {
|
||
orgName = nameList[0];
|
||
name = nameList[1];
|
||
}
|
||
|
||
var branchList = name.split('#');
|
||
if (branchList.length === 2) {
|
||
name = branchList[0];
|
||
branch = branchList[1];
|
||
}
|
||
|
||
var url = 'git@github.com:' + orgName + '/' + name;
|
||
var localPath = Path.join(localFolder, name);
|
||
if (!branch) branch = 'master';
|
||
var repoInfo = {
|
||
name: name,
|
||
branch: branch,
|
||
url: url,
|
||
localPath: localPath
|
||
};
|
||
return repoInfo;
|
||
}
|
||
|
||
function updateOriginUrl(path, newRemote, cb) {
|
||
var newUrl = 'git@github.com:' + newRemote + '/' + Path.basename(path);
|
||
exec(['remote', 'set-url', 'origin', newUrl], path, cb);
|
||
}
|
||
|
||
module.exports = {
|
||
exec: exec,
|
||
checkout: checkout,
|
||
checkoutRemoteBranch: checkoutRemoteBranch,
|
||
commit: commit,
|
||
pull: pull,
|
||
push: push,
|
||
getCurrentBranch: getCurrentBranch,
|
||
getCurrentCommit: getCurrentCommit,
|
||
reportStatus: reportStatus,
|
||
parseRepo: parseRepo,
|
||
updateOriginUrl: updateOriginUrl
|
||
};
|