UNPKG

create-git

Version:

Initalize a git repository with some helpful extras

362 lines (342 loc) 9.73 kB
'use strict' const path = require('path') const { promisify } = require('util') const cp = require('child_process') const execFile = promisify(cp.execFile) const opta = require('opta') const parseList = require('safe-parse-list') const fs = require('fs-extra') const got = require('got') const { Loggerr } = require('loggerr') const parseIgnore = require('./lib/ignore') function initOpts () { return opta({ commandDescription: 'Initalize a git repo', options: { cwd: { description: 'Directory to run in', prompt: false, flag: { alias: 'd', defaultDescription: 'process.cwd()' } }, silent: { type: 'boolean', prompt: false, flag: { conflicts: ['verbose'] } }, verbose: { type: 'boolean', prompt: false, flag: { conflicts: ['silent'] } }, primaryBranch: { description: 'Primary branch for repo', type: 'string', flag: { key: 'primary-branch', alias: 'b', defaultDescription: 'main' }, prompt: { message: 'Primary branch:', default: 'main' } }, switchToPrimaryBranch: { description: 'Switch to primary branch', type: 'string', flag: { key: 'switch-to-primary-branch' }, prompt: { group: 'switchToPrimaryBranch', message: 'Switch to primary branch before commit?', default: true } }, initialCommitMessage: { description: 'Message for initial commit', type: 'string', flag: { key: 'initial-commit-message', alias: 'm' }, prompt: { message: 'Initial commit message (leave empty for no commit):' } }, remoteOrigin: { description: 'Git remote origin', type: 'string', flag: { key: 'remote-origin', alias: 'o' }, prompt: { message: 'Set remote origin:' } }, switchToRemoteOrigin: { description: 'Switch to remote origin if different', type: 'boolean', flag: { key: 'switch-to-remote-origin' }, prompt: { group: 'switchtoRemoteOrigin', message: (ans, opts) => `Would you like to switch remote origin to point to ${opts.remoteOrigin}?`, type: 'confirm' } }, ignoreTemplates: { description: 'Ignore templates from github.com/github/gitignore', flag: { key: 'ignore-templates', alias: 't', defaultDescription: 'Node.gitignore' }, prompt: { message: 'Ignore templates', type: 'checkbox', default: ['Node.gitignore'], choices: [{ name: 'Node', value: 'Node.gitignore' }, { name: 'Sass', value: 'Sass.gitignore' }, { name: 'Vue', value: 'community/JavaScript/Vue.gitignore' }, { name: 'MacOs', value: 'Global/macOS.gitignore' }, { name: 'Linux', value: 'Global/Linux.gitignore' }, { name: 'Windows', value: 'Global/Windows.gitignore' }, { name: 'Vim', value: 'Global/Vim.gitignore' }, { name: 'Emacs', value: 'Global/Emacs.gitignore' }] } }, additionalRules: { description: 'Additional ignore rules to append to .gitignore', type: 'string', prompt: { message: 'Additional git ignores:', filter: parseList }, flag: { key: 'additional-rules', description: 'comma separated list of ignore lines' } }, ignoreExisting: { description: 'Ignore existing .gitignore and package.json files', prompt: false, flag: { key: 'ignore-existing', defaultDescription: 'false' } }, commitAll: { description: 'Commit all files (not just the new .gitignore', type: 'boolean', prompt: false, flag: { key: 'commit-all', defaultDescription: 'true' } }, push: { description: 'Push to remote origin when complete', type: 'boolean', prompt: false, flag: { defaultDescription: 'true' } } } }) } module.exports = main async function main (input, _opts = {}) { const options = initOpts() options.overrides({ cwd: input.cwd || process.cwd() }) options.overrides(input) let opts = options.values() const log = _opts.logger || new Loggerr({ level: (opts.silent && 'silent') || (opts.verbose && 'debug') || 'info', formatter: 'cli' }) const gitignorePath = path.join(opts.cwd, '.gitignore') // Load existing .gitignore let existingIgnoreStr let existingIgnore if (opts.ignoreExisting !== false) { try { existingIgnoreStr = await fs.readFile(gitignorePath, 'utf8') existingIgnore = parseIgnore.parse(existingIgnoreStr) } catch (e) { if (e.code !== 'ENOENT') { throw e } } try { const pkg = await fs.readJSON(path.join(opts.cwd, 'package.json')) if (pkg && pkg.repository && pkg.repository.type === 'git' && typeof pkg.repository.url === 'string') { options.defaults({ remoteOrigin: pkg.repository.url }) } } catch (e) { if (e.code !== 'ENOENT') { throw e } } } // Merge existing ignore with new ignore const ignoreRules = existingIgnore || parseIgnore.parse('') await options.prompt({ promptor: _opts.promptor })() opts = options.values() log.debug('Options:', opts) // Load templates for (const i in opts.ignoreTemplates) { try { const url = `https://raw.githubusercontent.com/github/gitignore/master/${opts.ignoreTemplates[i]}` log.debug(`Loading ignore template: ${url}`) const resp = await got(url) // Join sections ignoreRules.concat(resp.body) } catch (e) { log.error('Unable to load template', e) } } // Merge custom rules if (opts.additionalRules && opts.additionalRules.length) { ignoreRules.concat(opts.additionalRules) } // Create directory and init git log.info('Initalizing repo') await fs.ensureDir(opts.cwd) await git(['init', '-b', opts.primaryBranch], { cwd: opts.cwd, log }) // Switch to primary branch const currentBranch = (await git(['branch', '--show-current'], { cwd: opts.cwd, log })).stdout.trim() if (currentBranch && currentBranch !== opts.primaryBranch) { const { switchToPrimaryBranch } = await options.prompt({ promptor: _opts.promptor, groups: ['switchToPrimaryBranch'] })() if (switchToPrimaryBranch) { log.info(`Checking out ${opts.primaryBranch}`) await git(['checkout', '-b', opts.primaryBranch], { cwd: opts.cwd, log }) } } if (opts.remoteOrigin) { try { log.info(`Adding remote origin ${opts.remoteOrigin}`) await git(['remote', 'add', 'origin', opts.remoteOrigin], { cwd: opts.cwd, log }) } catch (e) { // If remote already exists, test if it is the same, if so, move on, else throw if (e.stderr.includes('already exists')) { const url = (await git(['remote', 'get-url', 'origin'], { cwd: opts.cwd, log })).stdout.trim() if (url !== opts.remoteOrigin) { log.error(`remote origin already exists and points somewhere else: ${url}`) const { switchToRemoteOrigin } = await options.prompt({ promptor: _opts.promptor, groups: ['switchToRemoteOrigin'] })(opts) if (switchToRemoteOrigin) { await git(['remote', 'rm', 'origin'], { cwd: opts.cwd, log }) await git(['remote', 'add', 'origin', opts.remoteOrigin], { cwd: opts.cwd, log }) } } } else { throw e } } } // Write gitignore log.info('Writing .gitignore') await fs.writeFile(gitignorePath, parseIgnore.stringify(ignoreRules)) if (opts.initialCommitMessage) { log.info('Committing changes') if (opts.commitAll !== false) { await git(['add', '.'], { cwd: opts.cwd, log }) } else { await git(['add', gitignorePath], { cwd: opts.cwd, log }) } try { await git(['commit', '-m', opts.initialCommitMessage], { cwd: opts.cwd, log }) } catch (e) { if (e.stdout.includes('nothing to commit')) { // Ignore error, but log log.info('No changes to commit, skipping commit') } else { throw e } } } if (opts.push !== false) { log.info('Pushing changes to remote origin') await git(['push'], { cwd: opts.cwd }) } } module.exports.options = initOpts().options module.exports.cli = function () { return initOpts().cli((yargs) => { yargs.command('$0', 'initalize a git repo', () => {}, main) }) } module.exports.execGit = git async function git (args, opts) { try { opts.log && opts.log.debug(`git ${args.join(' ')}`, { cwd: opts.cwd }) const ret = await execFile('git', args, opts) return ret } catch (e) { Error.captureStackTrace(e, git) e.message = `${e.message}${e.stdout}${e.stderr}` throw e } }