@deliverymanager/gitsync
Version:
Synchronize multiple git repos recursively
475 lines (407 loc) • 14 kB
JavaScript
;
const Promise = require('bluebird');
const exec = Promise.promisify(require('child_process').exec, {
multiArgs: true
});
const fs = Promise.promisifyAll(require('fs'));
const homedir = require('os').homedir();
const colors = require('colors/safe');
const path = require('path');
const minimist = require('minimist');
const _ = require('lodash');
const Spinner = require('cli-spinner').Spinner;
const prompt = require('prompt-promise');
const Clone = require('./src/clone');
const clone = new Clone();
const pull = require('./src/pull');
//console.log('clone:', clone.clone_repo);
module.exports = function () {
process.env.UV_THREADPOOL_SIZE = 10;
const args = minimist(process.argv.slice(2), {
boolean: ['all', 'a', 'pull', 'clone', 'init', 'checkout', 'verbose']
});
const start = process.hrtime();
const invalid_desc_repos = [];
const err_repos = [];
const diverged = [];
const ahead = [];
const no_remote = [];
const unable_to_checkout = [];
const unsaved_changes = [];
const repos_pulled = [];
let repos_cloned = [];
const repos_checked_out = [];
// util
/**
* get_sync_info
*
* @return {type}
*/
function get_sync_info() {
/**
* init - description
*
* @param {type} cred
* @return {type}
*/
function init(cred) {
console.log('Creating Configuration File..');
if (!cred) cred = {};
return prompt('User agent: ')
.then(user_agent => {
cred.user_agent = user_agent;
return prompt('Sync account: ');
})
.then(sync_account => {
cred.sync_account = sync_account;
return prompt('Is account an organization? (no): ');
})
.then(org => {
cred.org = org.toLowerCase().indexOf('y') !== -1;
return prompt('Default Root Folder: ');
})
.then(default_root_path => {
default_root_path = path.resolve(default_root_path);
cred.default_root_path = default_root_path;
const data = JSON.stringify(cred, null, '\t');
return fs.writeFileAsync(`${homedir}/.gitsync.json`, data);
})
.then(() => {
if (cred.default_root_path) process.chdir(cred.default_root_path);
return cred;
});
}
if (args.init) {
return fs.readFileAsync(`${homedir}/.gitsync.json`, 'utf-8')
.then(data => init(JSON.parse(data)).then(clone.init_cred))
.catch(err => {
if (err.code != 'ENOENT') throw err;
else return init().then(clone.init_cred);
});
} else {
return fs.readFileAsync(`${homedir}/.gitsync.json`, 'utf-8')
.then(data => {
data = JSON.parse(data);
if (args.account) data.sync_account = args.account;
if (args.org) data.org = args.org;
if (args.user) data.user_agent = args.user;
if (args.cwd) data.default_root_path = args.cwd;
if (data.default_root_path) process.chdir(data.default_root_path);
if (data.user_agent && data.sync_account) return data;
else return init(data);
})
.catch(err => {
if (err.code != 'ENOENT') throw err;
else return init().then(clone.init_cred);
});
}
}
/**
* pull_local
*
* @param {type} local_repos
* @return {type}
*/
function pull_local(local_repos) {
console.log('local_repos:', local_repos.length);
// const root_dir = process.cwd();
// console.log('root_dir:', root_dir);
return Promise.map(local_repos, repo_path => {
// console.log('repo_path:', repo_path);
const full_path = path.resolve(repo_path);
return pull.get_status(full_path)
.then(status => {
// console.log('status:', status);
switch (status) {
case 'fast-forward':
return pull.pull_all(full_path)
.then(() => {
repos_pulled.push(full_path);
return;
})
.catch(() => {
err_repos.push(repo_path);
return;
});
case 'diverged':
diverged.push(full_path);
return;
case 'ahead':
ahead.push(full_path);
return;
case 'no-remote':
no_remote.push(full_path);
return;
case 'Untracked':
unsaved_changes.push(full_path);
return;
case 'Changes to be committed':
unsaved_changes.push(full_path);
return;
default:
// console.log(colors.green(`Repo ${full_path} is up-to-date\n`));
return;
}
});
}, {
concurrency: 20
})
.then(() => local_repos);
}
/**
* filter_repos
*
* @param {type} remote_repos
* @param {type} local_repos
* @return {type}
*/
function filter_repos(remote_repos, local_repos) {
const repos_to_clone = [];
// remove local repos from remote repos
remote_repos = _.filter(remote_repos, remote_repo => {
const foundIndex = local_repos.findIndex(local_repo => {
if (local_repo.slice(local_repo.lastIndexOf('/') !== -1 ? (local_repo.lastIndexOf('/') + 1) : 0) === remote_repo.name) {
return true;
}
});
if (foundIndex === -1) {
console.log("\nremote_repo not found:", remote_repo);
return true;
}
});
remote_repos.forEach(repo => {
// console.log('repo.local_path:', repo.local_path);
if (repo.local_path && clone.is_path_valid(repo.local_path)) {
// console.log('valid repo desc:', colors.green(repo.local_path));
repos_to_clone.push(repo);
} else if (!repo.local_path || repo.local_path.indexOf('ignore:') === -1) {
// console.log('invalid repo desc:', colors.red(repo.local_path));
invalid_desc_repos.push(repo);
}
});
// console.log('\nremote_repos:', repos_to_clone.length);
// console.log('\nlocal_repos:', local_repos.length);
return {
remote_repos: repos_to_clone,
local_repos: local_repos
};
}
/**
* checkout - description
*
* @param {type} branches description
* @param {type} full_path description
* @return {type} description
*/
function checkout(branches, full_path) {
// console.log('branches: ', branches);
let stable_branch;
if (branches.indexOf('prod') !== -1) stable_branch = 'prod';
else if (branches.indexOf('production') !== -1) stable_branch = 'production';
// console.log('stable_branch:', stable_branch);
if (!stable_branch) return Promise.resolve();
else return exec(`git checkout ${stable_branch}`, {
maxBuffer: 1024 * 1024 * 1024,
cwd: full_path
})
// stderr not really err
.spread((stderr, stdout) => {
// console.log('stdout:', stdout);
if (stdout.indexOf('Already on') === -1) repos_checked_out.push(full_path);
return Promise.resolve();
})
.catch(err => {
if (args.verbose) console.log(`Path: ${full_path}, err:${err}`);
unable_to_checkout.push(full_path);
return Promise.resolve();
});
}
//
/**
* create_missing_branches
*
* @param {type} local_repos
* @return {type}
*/
function create_missing_branches(local_repos) {
return Promise.map(local_repos, repo_path => {
// console.log('repo_path:', repo_path);
const full_path = path.resolve(repo_path);
//console.log("full_path", full_path);
return pull.get_all_branches(full_path)
.then(branches => pull.track_missing_branches(branches, full_path))
.then(branches_added => checkout(branches_added, full_path));
}, {
concurrency: 20
});
}
/**
* all
*
* @return {type}
*/
function all() {
return get_sync_info()
.then(cred => {
let spinner = new Spinner('Getting remote and local repos....');
spinner.start();
return Promise.all([
clone.get_all_repos_names(cred.sync_account, cred.org, cred.user_agent, cred.at),
pull.get_existing_repos()
])
// clone all remotes that do not exist locally
.then(results => {
spinner.stop();
const remote_repos = results[0];
const local_repos = results[1];
const filtered_repos = filter_repos(remote_repos, local_repos);
repos_cloned = filtered_repos.remote_repos;
// used for testing
// return local_repos;
spinner = new Spinner('Cloning missing repos, Pulling, Creating Branches....');
spinner.start();
return Promise.map(filtered_repos.remote_repos, clone.clone_repo, {
concurrency: 20
})
.then(() => create_missing_branches(filtered_repos.remote_repos.map(repo => `${repo.local_path}/${repo.name}`)))
.then(() => Promise.resolve(filtered_repos.local_repos));
})
// pull all local repos that need to update
.then(pull_local)
.then(create_missing_branches)
.then(() => spinner.stop());
});
}
/**
* pull_repos - description
*
* @return {type} description
*/
function pull_repos() {
let spinner;
return get_sync_info()
.then(() => {
spinner = new Spinner('Finding existing repos and pulling...');
spinner.start();
return pull.get_existing_repos();
})
.then(pull_local)
.then(create_missing_branches)
.then(() => spinner.stop());
}
/**
* clone_repos - description
*
* @return {type} description
*/
function clone_repos() {
return get_sync_info()
.then(cred => {
return Promise.all([
clone.get_all_repos_names(cred.sync_account, cred.org, cred.user_agent, cred.at),
pull.get_existing_repos()
]);
})
// clone all remotes that do not exist locally
.then(results => {
const remote_repos = results[0];
const local_repos = results[1];
const filtered_repos = filter_repos(remote_repos, local_repos);
repos_cloned = filtered_repos.remote_repos;
// used for testing
// return local_repos;
const spinner = new Spinner('Cloning missing repos and creating branches....');
spinner.start();
console.log('cloning repos');
return Promise.map(filtered_repos.remote_repos, clone.clone_repo, {
concurrency: 20
})
.then(() => create_missing_branches(filtered_repos.remote_repos.map(repo => `${repo.local_path}/${repo.name}`)))
.then(() => spinner.stop());
});
}
/**
* checkout_stable - description
*
* @return {type} description
*/
function checkout_stable() {
const spinner = new Spinner('Checking out to stable branches...');
spinner.start();
return get_sync_info()
.then(pull.get_existing_repos)
.then(repos => {
return Promise.map(repos, repo_path => {
//const full_path = path.resolve(repo_path);
// console.log('full_path:', full_path);
return pull.get_all_branches(full_path)
.then(branches => checkout(branches, full_path));
}, {
concurrency: 20
});
})
.then(() => spinner.stop());
}
//Starting the functions
Promise.try(function () {
switch (true) {
case args.init:
return get_sync_info();
case (args.all || args.a):
return all();
case args.pull:
return pull_repos();
case args.clone:
return clone_repos();
case args.checkout:
return checkout_stable();
default:
return Promise.reject('Arguments required: --pull to pull repos, --clone to clone repos, --all to both pull and clone, --checkout to checkout to stable branches');
}
})
.then(() => {
console.log('\n');
if (invalid_desc_repos.length > 0) {
console.log(colors.red('\nInvalid Description Repos:'));
invalid_desc_repos.forEach(repo => {
console.log(`Repo Name: ${repo.name}`);
console.log(`Description: ${colors.red(repo.local_path)}`);
});
}
if (err_repos.length > 0) {
console.log(colors.red('\nRepos that returned error:'));
console.log(err_repos);
}
if (diverged.length > 0) {
console.log(colors.red('\nRepos that have diverged:'));
console.log(diverged);
}
if (unable_to_checkout.length > 0) {
console.log(colors.red('\nRepos that were unable to checkout to stable branch:'));
console.log(unable_to_checkout);
}
if (ahead.length > 0) {
console.log(colors.green('\nRepos that are ahead of remote:'));
console.log(ahead);
}
if (no_remote.length > 0) {
console.log(colors.yellow('\nRepos that have no remote:'));
console.log(no_remote);
}
if (unsaved_changes.length > 0) {
console.log(colors.rainbow('\nRepos that have unsaved changes:'));
console.log(unsaved_changes);
}
if (!args.clone && !args.init && !args.checkout) console.log('\nrepos pulled:', repos_pulled);
if (!args.pull && !args.init && !args.checkout) console.log('\nrepos cloned:', repos_cloned);
if (args.checkout) console.log('\nrepos checked out: ', repos_checked_out);
console.log(colors.green('\nSuccess'));
console.log('seconds:', process.hrtime(start)[0]);
process.exit();
}).catch(err => {
console.log(colors.red(err));
console.log('seconds:', process.hrtime(start)[0]);
process.exit(1);
});
};