diff --git a/configGit.js b/configGit.js new file mode 100644 index 0000000..64ba192 --- /dev/null +++ b/configGit.js @@ -0,0 +1,45 @@ +'use strict' + +const rRepoURL = /^(?:(?:git|https?|git\+https|git\+ssh):\/\/)?(?:[^@]+@)?([^\/]+?)[\/:](.+?)\.git$/ // eslint-disable-line no-useless-escape +const rGithubPage = /\.github\.(io|com)$/ + +function parseRepo(repo) { + const split = repo.split(',') + const url = split.shift() + let branch = split[0] + + if (!branch && rRepoURL.test(url)) { + const match = url.match(rRepoURL) + const host = match[1] + const path = match[2] + + if (host === 'github.com') { + branch = rGithubPage.test(path) ? 'master' : 'gh-pages' + } else if (host === 'coding.net') { + branch = 'coding-pages' + } + } + + return { + url: url, + branch: branch || 'master' + } +} + +module.exports = function(args) { + const repo = args.repo || args.repository + if (!repo) throw new TypeError('repo is required!') + + if (typeof repo === 'string') { + const data = parseRepo(repo) + data.branch = args.branch || data.branch + + return [data] + } + + const result = Object.keys(repo).map(key => { + return parseRepo(repo[key]) + }) + + return result +} diff --git a/deploy.js b/deploy.js index 06361a1..2817567 100644 --- a/deploy.js +++ b/deploy.js @@ -1,12 +1,17 @@ const fs = require('fs') const path = require('path') -const ssh = new (require('node-ssh'))() /** ******************* 读取命令行以及配置文件里的参数 ******************** **/ const commander = require('commander') const deepmerge = require('deepmerge') -var Config = {} +/** + * repo: 'git@github.com:fivapp/fivapp.github.io.git', + name: 'Limo Saplf', + email: 'limosaplf@gmail.com', + */ + +var Config = { deploy: {} } // 读取配置文件 try { @@ -29,28 +34,26 @@ try { commander .version('1.0', '-v, --version') // 默认是 -V。如果要 -v,就要加 '-v --version' + .option('-t, --type ', `Deploy to server type, web or git. Default to ${Config.deploy.type}`) .option('-H, --host ', `Host IP or domain name of the target server. Default to ${Config.deploy.host}`) + .option('-D, --dir ', `Directory to deploy on the target server. Default to ${Config.deploy.dir}`) + .option('-d, --dist ', `Folder to deploy on the target server. Default to ${Config.deploy.dist}`) + .option('-l, --local ', `Local folder to copy from. Default to ${Config.deploy.local}`) .option('-P, --port ', `Ssh port number of the target server. Default to ${Config.deploy.port}`) .option('-u, --user ', `User id to login the target server. Default to ${Config.deploy.user}`) .option('-k, --key ', `User private key file to login the target server. Default to ${Config.deploy.key}`) .option('-p, --password ', `User password to login the target server. You may have to enclose it in "". Default to "${Config.deploy.password}"`) - .option('-l, --local ', `Local folder to copy from. Default to ${Config.deploy.local}`) - .option('-D, --dir ', `Directory to deploy on the target server. Default to ${Config.deploy.dir}`) - .option('-d, --dist ', `Folder to deploy on the target server. Default to ${Config.deploy.dist}`) .parse(process.argv) -const root = commander.root || Config.deploy.root // 本地的项目目录。似乎该目录必须已经存在于服务器上 -console.log(` root = ${root} `) -const dist = commander.dist || Config.deploy.dist || 'dist' // 新系统将发布在这个目录里。建议为dist,和npm run build产生的目录一致,这样既可以远程自动部署,也可以直接登录服务器手动部署。 -console.log(` dist = ${dist} `) const privateKeyFile = commander.key || Config.deploy.key || `${process.env.HOME}/.ssh/id_rsa` -console.log(` privateKeyFile = ${privateKeyFile} `) -const local = commander.local || Config.deploy.local || 'dist' -console.log(` local = ${local} `) const connection = { + type: commander.type || Config.deploy.type || 'web', host: commander.host || Config.deploy.host, port: commander.port || Config.deploy.port || 22, + dir: commander.dir || Config.deploy.dir, // 目标服务器上的目录。似乎该目录必须已经存在于服务器上 + dist: commander.dist || Config.deploy.dist || 'dist', // 新系统将发布在这个文件夹里。建议为dist,和npm run build产生的目录一致,这样既可以远程自动部署,也可以直接登录服务器手动部署。 + local: commander.local || Config.deploy.local || 'dist', username: commander.user || Config.deploy.user, privateKey: fs.existsSync(privateKeyFile) ? privateKeyFile : undefined, password: commander.password || Config.deploy.password, @@ -63,62 +66,230 @@ const connection = { } console.log(` connection = ${JSON.stringify(connection)}`) -/** ********************** 连接到待部署的主机,拷贝文件到指定路径 ************* **/ -function subDirs (path) { - const dirs = [path] - if (fs.statSync(path).isFile()) { +if (connection.type==='web') { + deployToWeb() +}else if (connection.type==='git'){ + deployToGit() +} + +/** ********************** 连接到 Web主机,拷贝文件到指定路径 ************* **/ +function deployToWeb(){ + const ssh = new (require('node-ssh'))() + + function subDirs (path) { + const dirs = [path] + if (fs.statSync(path).isFile()) { + return dirs + } + fs.readdirSync(path).forEach(item => { + const stat = fs.statSync(`${path}/${item}`) + if (stat.isDirectory()) { + dirs.push(...subDirs(`${path}/${item}`)) + } + }) return dirs } - fs.readdirSync(path).forEach(item => { - const stat = fs.statSync(`${path}/${item}`) - if (stat.isDirectory()) { - dirs.push(...subDirs(`${path}/${item}`)) + + const necessaryPath = (path) => { + return subDirs(path) + .map(it => it.replace(path, '')) + .filter(it => it) + .map(it => it.split('/').filter(it => it)) + } + + ssh.connect(connection).then(async () => { + console.log(`[ mv ${connection.dist} ${connection.dist}-backup-${new Date().toISOString()} ... ]`) + await ssh.execCommand(`mv ${connection.dist} ${connection.dist}-backup-${new Date().toISOString()}`, { cwd: connection.dir }) + console.log(`[ mkdir ${connection.dist} ... ]`) + await ssh.execCommand(`mkdir ${connection.dist}`, { cwd: connection.dir }) + const toCreate = necessaryPath('./'+connection.local) + for (const name of toCreate) { + console.log(`[ mkdir ${connection.dist}/${name.join('/')} ... ]`) + await ssh.execCommand(`mkdir ${connection.dist}/${name.join('/')}`, { cwd: connection.dir }) } - }) - return dirs -} - -const necessaryPath = (path) => { - return subDirs(path) - .map(it => it.replace(path, '')) - .filter(it => it) - .map(it => it.split('/').filter(it => it)) -} - -ssh.connect(connection).then(async () => { - console.log(`[ mv ${dist} ${dist}-backup-${new Date().toISOString()} ... ]`) - await ssh.execCommand(`mv ${dist} ${dist}-backup-${new Date().toISOString()}`, { cwd: root }) - console.log(`[ mkdir ${dist} ... ]`) - await ssh.execCommand(`mkdir ${dist}`, { cwd: root }) - const toCreate = necessaryPath('./'+local) - for (const name of toCreate) { - console.log(`[ mkdir ${dist}/${name.join('/')} ... ]`) - await ssh.execCommand(`mkdir ${dist}/${name.join('/')}`, { cwd: root }) - } - - let err - console.log(`[ Upload to ${root}/${dist} ... ]`) - await ssh.putDirectory('./'+local, `${root}/${dist}`, { - concurrency: 10, - recursive: true, - validate: itemPath => { - const baseName = path.basename(itemPath) - return !baseName.endsWith('.map') - }, - tick: (localPath, remotePath, error) => { - console.log(`Uploading "${localPath}" ===> "${remotePath}" ${error || 'succeeded!'}`) - err = error - }, - }) - ssh.dispose() - if (err) { - console.log('[ Uploaded with error! ]') + + let err + console.log(`[ Upload to ${connection.dir}/${connection.dist} ... ]`) + await ssh.putDirectory('./'+connection.local, `${connection.dir}/${connection.dist}`, { + concurrency: 10, + recursive: true, + validate: itemPath => { + const baseName = path.basename(itemPath) + return !baseName.endsWith('.map') + }, + tick: (localPath, remotePath, error) => { + console.log(`Uploading "${localPath}" ===> "${remotePath}" ${error || 'succeeded!'}`) + err = error + }, + }) + ssh.dispose() + if (err) { + console.log('[ Uploaded with error! ]') + process.exit(1) + } else { + console.log('[ Uploaded successfully! ]') + } + }).catch(err => { + console.error(err) + ssh.dispose() process.exit(1) - } else { - console.log('[ Uploaded successfully! ]') + }) +} + +/** ********************** 连接到 Git主机,拷贝文件到指定路径 ************* **/ +function deployToGit(){ + const pathFn = require('path') + const fs = require('hexo-fs') + const chalk = require('chalk') + const swig = require('swig-templates') + const moment = require('moment') + const Promise = require('bluebird') + const spawn = require('hexo-util/lib/spawn') + const parseConfig = require('./configGit') + + const swigHelpers = { + now: function(format) { + return moment().format(format) + } } -}).catch(err => { - console.error(err) - ssh.dispose() - process.exit(1) -}) + + function exec() { + const baseDir = '' + const deployDir = pathFn.join(baseDir, '.deploy_git') + const publicDir = connection.dist + let extendDirs = connection.extend_dirs + const ignoreHidden = connection.ignore_hidden + const ignorePattern = connection.ignore_pattern + const message = commitMessage(connection) + const verbose = !connection.silent + + if (!connection.repo && process.env.HEXO_DEPLOYER_REPO) { + connection.repo = process.env.HEXO_DEPLOYER_REPO + } + + if (!connection.repo && !connection.repository) { + let help = '' + + help += 'You have to configure the deployment settings in config files or command line first!\n\n' + help += 'Example:\n' + help += ' deploy:\n' + help += ' type: git\n' + help += ' repo: \n' + help += ' branch: [branch]\n' + help += ' message: [message]\n\n' + help += ' extend_dirs: [extend directory]\n\n' + help += 'For more help, you can check the docs: ' + chalk.underline('http://hexo.io/docs/deployment.html') + + console.log(help) + return + } + + function git(...connection) { + return spawn('git', connection, { + cwd: deployDir, + verbose: verbose, + stdio: 'inherit' + }) + } + + function setup() { + const userName = connection.name || connection.user || connection.userName || '' + const userEmail = connection.email || connection.userEmail || '' + + // Create a placeholder for the first commit + return fs.writeFile(pathFn.join(deployDir, 'placeholder'), '').then(() => { + return git('init') + }).then(() => { + return userName && git('config', 'user.name', userName) + }).then(() => { + return userEmail && git('config', 'user.email', userEmail) + }).then(() => { + return git('add', '-A') + }).then(() => { + return git('commit', '-m', 'First commit') + }) + } + + function push(repo) { + return git('add', '-A').then(() => { + return git('commit', '-m', message).catch(() => { + // Do nothing. It's OK if nothing to commit. + }) + }).then(() => { + return git('push', '-u', repo.url, 'HEAD:' + repo.branch, '--force') + }) + } + + return fs.exists(deployDir).then(function(exist) { + if (exist) return + + // log.info('Setting up Git deployment...') + return setup() + }).then(() => { + // log.info('Clearing .deploy_git folder...') + return fs.emptyDir(deployDir) + }).then(() => { + const opts = {} + // log.info('Copying files from public folder...') + if (typeof ignoreHidden === 'object') { + opts.ignoreHidden = ignoreHidden.public + } else { + opts.ignoreHidden = ignoreHidden + } + + if (typeof ignorePattern === 'string') { + opts.ignorePattern = new RegExp(ignorePattern) + } else if (typeof ignorePattern === 'object' && Reflect.apply(Object.prototype.hasOwnProperty, ignorePattern, ['public'])) { + opts.ignorePattern = new RegExp(ignorePattern.public) + } + + return fs.copyDir(publicDir, deployDir, opts) + }).then(() => { + // log.info('Copying files from extend dirs...') + + if (!extendDirs) { + return + } + + if (typeof extendDirs === 'string') { + extendDirs = [extendDirs] + } + + const mapFn = function(dir) { + const opts = {} + const extendPath = pathFn.join(baseDir, dir) + const extendDist = pathFn.join(deployDir, dir) + + if (typeof ignoreHidden === 'object') { + opts.ignoreHidden = ignoreHidden[dir] + } else { + opts.ignoreHidden = ignoreHidden + } + + if (typeof ignorePattern === 'string') { + opts.ignorePattern = new RegExp(ignorePattern) + } else if (typeof ignorePattern === 'object' && Reflect.apply(Object.prototype.hasOwnProperty, ignorePattern, [dir])) { + opts.ignorePattern = new RegExp(ignorePattern[dir]) + } + + return fs.copyDir(extendPath, extendDist, opts) + } + + return Promise.map(extendDirs, mapFn, { + concurrency: 2 + }) + }).then(() => { + return parseConfig(connection) + }).each(function(repo) { + return push(repo) + }) + } + + function commitMessage(connection) { + const message = connection.m || connection.msg || connection.message || 'Site updated: {{ now(\'YYYY-MM-DD HH:mm:ss\') }}' + return swig.compile(message)(swigHelpers) + } + + exec() + +} \ No newline at end of file diff --git a/package.json b/package.json index c981bd7..2604f96 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,14 @@ "author": "", "license": "ISC", "dependencies": { + "bluebird": "^3.5.5", + "chalk": "^2.4.2", "commander": "^3.0.1", "deepmerge": "^4.0.0", - "node-ssh": "^6.0.0" + "hexo-fs": "^2.0.0", + "hexo-util": "^1.1.0", + "moment": "^2.24.0", + "node-ssh": "^6.0.0", + "swig-templates": "^2.0.3" } }