mirror of
https://github.com/smallmain/cocos-enhance-kit.git
synced 2024-12-26 11:48:29 +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
|
|||
|
};
|