前言
首先回顾一下,手动操作的话流程如下:
- 通过
git init
命令在当前目录初始化本地git仓库 - 创建remote仓库
- 将本地仓库关联到remote仓库
- 创建
.gitignore
文件 - 提交本地文件并推送到remote仓库
接下来将会按照这个流程来一步步实现CLI。
依赖项
会用到以下这些依赖性:
- axios: Promise based HTTP client(http客户端)
- chalk: colorizes the output(让命令行输出有颜色)
- clui: draws command-line tables, gauges and spinners(在命令行画菊花 :D)
- configstore: easily loads and saves config without you having to think about where and how. (读取和写入配置文件的客户端)
- figlet: creates ASCII art from text(以艺术字的形式展示文字)
- inquirer: creates interactive command-line user interface(创建交互式命令行界面)
- lodash
- minimist: parses argument options(转换命令行参数)
- request: Http request client
- simple-git: a tool for running Git commands in a Node.js application(在nodejs中快速执行git命令)
初始化项目
先创建一个名为Yginit
的目录,在该目录下执行npm init
,按照提示一步一步生成package.json
文件,然后安装相关依赖性。
mkdir Yginit
cd Yginit
npm install --save axios chalk clui configstore figlet inquirer lodash minimist request simple-git
添加一些帮助类
在根目录添加lib
文件夹,并添加以下四个帮助类:
- files.js: 文件相关的管理方法
- inquirer.js: 命令行交互方法
- git.js: git操作相关方法
- repo.js: repo操作相关方法
files.js
接下来,让我们从lib\files.js
开始,
这个帮助类需要提供两个方法:
- 获取当前文件夹的名称(用于repository的默认名称)
- 判断目录是否存在(通过检查当前文件夹下是否存在
.git
文件夹来判断当前文件夹是否已经是一个Git Repository了)
首先,你可能会尝试使用fs
module的realPathSync
方法来获取当前目录:
path.basename(path.dirname(fs.realpathSync(__filename)));
当我们在和这个应用相同文件夹时,使用这个方法是没问题的,(e.g: node index),但是,要牢记我们这个应用是个全局应用,这意味着我们需要获取的是执行当前命令所在的文件夹,而不是这个应用所在的文件夹。因此,更好的选择是使用process.cwd()
其次,使用fs.statSync
来判断文件是否存在时,如果文件不存在,会抛出异常,因此在这里要使用try...catch...
来包裹。
最后,在一个命令行应用中,选择同步
版本的命令是个更好的选择(比如用fs.statSync
)
最终,lib\files.js
的内容如下:
const fs = require('fs');
const path = require('path');
module.exports = {
getCurrentDirectoryBase () {
return path.basename(process.cwd());
},
directoryExists (filePath) {
try {
return fs.statSync(filePath).isDirectory();
} catch (err) {
return false;
}
}
};
回到index.js
,引入这个文件:
const files = require('./lib/files');
接下来,我们就要开始开发CLI Application的具体内容了。
初始化CLi
为了让用户更好的交互,首先让我们清理下命令行的输出,并显示一个Banner。
// 清除屏幕
clear();
// 打印Banner
console.log(
chalk.yellow(
figlet.textSync('Welcome To Use YgInit', {
// font: 'Ghost',
horizontalLayout: 'full',
verticalLayout: 'default'
})
)
);
备注:这里用到3个第三方库,clear
顾名思义用来清理console的output,chalk
用来在console输出有颜色的文字,figlet
用来生成艺术字。
接下来,使用上面的files.js
中提供的方法来判断是否包含.git
文件夹,如果包含,则结束。
// 判断是否存在.git
if (files.directoryExists('.git')) {
console.log(chalk.red('Already a git repository!'));
process.exit();
} else {
console.log(chalk.blue('let\'s begin!'));
}
和用户交互
接下来就要采用QA
的方式来和用户进行交互,收集所需的信息了。
创建lib\inquirer.js
,这个文件用来处理和用户的交互
inquirer.prompt
会问用户一系列问题,拿到的结果就是用户输入的信息。
// 仓库信息交互
askRepoDetails (groupList) {
const argv = require('minimist')(process.argv.slice(2), {
string: ['name', 'desc'],
alias: {
name: 'n',
desc: 'd'
}
});
const questions = [
{
name: 'group',
type: 'list',
message: 'Choise Group:',
pageSize: 20,
choices: groupList.map(item => {
return {
name: item.path + '(' + item.description + ')',
value: item.id
}
}),
validate (val) {
if (val) {
return true;
} else {
return 'Please Choice a Group';
}
}
},
{
type: 'input',
name: 'reponame',
message: 'Enter a name for the repository:',
default: argv.name || files.getCurrentDirectoryBase(),
validate (val) {
if (val) {
return true;
} else {
return 'Please Enter Repo Name';
}
}
},
{
type: 'input',
name: 'repodesc',
message: 'Optionally enter desc for the repository:',
default: argv.desc || null
},
{
type: 'list',
name: 'repotype',
message: 'Public or private',
choices: ['public', 'private'],
default: 'private'
}
];
return inquirer.prompt(questions);
}
关于inquirer
的详细用法可以看官方文档
一般问题有以下属性:
- name: 必填项,返回答案的key值
- type: 这里只用到了input、list(单选)、checkbox(多选)、password
- message: 问题
- choice: 如果是list、checkbox等的话的候选项
- default: 默认值
- validate: 验证
上面还用到了minimist
这个lib,主要是用来转换参数,还可以给参数起别名。
处理和Git的交互
和gitlab的交互都在这个文件夹中,主要有通过用户名密码获取private_token
,获取群组信息。
module.exports = {
registerNewToken (userInfo, url) {
// 调用api,获取private_token,并存入configstore中
},
getStoredToken () {
// 从configstore中获取private_token
},
registerNewGitUrl (url) {
// 将git_url存入configstore中
},
getStoredGitUrl () {
// 从configstore中获取git_url
},
getGroup (url) {
// 获取当前用户有权限看到的群组列表
}
};
config store
通过以下方法获取configstore的实例:
const ConfigStore = require('configstore');
const pkg = require('../package.json');
const conf = new ConfigStore(pkg.name);
然后很简单的使用get
和set
就可以从configstore中读取或者写入配置项。这样下次如果判断之前有保存,就可以重新直接读取了。
创建远程仓库
在拿到gitlab的private_token之后,就可以创建remote仓库了。
首先让用户选择项目所属群组,然后输入项目的基本信息,再调用api即可创建远程仓库
module.exports = {
// create remote repo
createRemoteRepo (url) {
return new Promise(async (resolve, reject) => {
const baseUrl = `http://${url}/api/v3`;
// 获取群组列表
const groupList = await gitlab.getGroup(baseUrl);
// 获取选择的群组
const answers = await inquirer.askRepoDetails(groupList);
const postData = {
name: answers.reponame,
path: answers.reponame,
namespace_id: answers.group,
description: answers.repodesc,
public: answers.repotype === 'public'
}
const status = new Spinner('Creating remote repository...');
status.start();
// 创建remote repository
axios.post(`${baseUrl}/projects/?private_token=${conf.get(pkg.name)}`, postData)
.then(res => {
status.stop();
console.log(`repo created...url:${res.data.web_url}`);
resolve(res.data.http_url_to_repo);
})
.catch(err => {
console.log(err.toString());
status.stop();
reject();
});
});
}
}
创建.gitignore文件
根据用户选择的项目类型(e.g:node、visual stuiod、maven、java),从github/gitignore下载对应的.gitignore
文件,保存在当前文件夹。
目前还很简单,也只支持4中项目类型。
// 创建.gitignore文件
async createGitIgnore () {
// 获取项目类型:比如node,java,maven,visual studio等
const ignoreInfo = await inquirer.askIgnoreFiles();
const protype = ignoreInfo.protype;
// 根据项目类型获取.gitignore文件
let downloadUrl = '';
switch (protype) {
case 'visual studio':
downloadUrl = 'https://raw.githubusercontent.com/github/gitignore/master/VisualStudio.gitignore';
break;
case 'node':
downloadUrl = 'https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore';
break;
case 'maven':
downloadUrl = 'https://raw.githubusercontent.com/github/gitignore/master/Maven.gitignore';
break;
case 'java':
downloadUrl = 'https://raw.githubusercontent.com/github/gitignore/master/Java.gitignore';
break;
}
// 下载并生成.gitignore文件
return new Promise((resolve, reject) => {
const ws = fs.createWriteStream('.gitignore');
request(downloadUrl).pipe(ws);
ws.on('finish', () => {
resolve();
});
ws.on('error', () => {
reject();
})
});
}
设置本地Git
接下来使用simple-git
来完成以下几步操作:
- 运行
git init
- 添加
.gitignore
- 添加其他所有文件
- 运行
commit
- 将新创建的git增加到remote repository
- git push
async setupRepo (url) {
const status = new Spinner('Initializing local repository and pushing to remote...');
status.start();
try {
// 利用simple-git链式编程依次执行
// git init
// git add .gitignore
// git add ./*
// git commit -m "Initial commit"
// git remote add remote-url
// git push origin master
await git
.init()
.add('.gitignore')
.add('./*')
.commit('Initial commit')
.addRemote('origin', url)
.push('origin', 'master');
return true;
} catch (err) {
console.log(err.toString());
status.stop();
return false;
} finally {
status.stop();
}
}
最终效果
将上面这些操作合在一起之后,index.js
的伪代码如下:
// 1.0 清除屏幕
// 2.0 打印Banner
// 3.0 判断当前文件夹是否有.git文件夹,如果已存在则提示当前已经存在一个git repository了
// 4.0 创建仓库
// 4.1 交互式方式获取gitlab地址
// 4.2 交互式方式获取用户密码,登陆gitlab换取private_token
// 4.3 创建remote仓库
// 4.4 创建.gitignore文件
// 4.5 初始化仓库
// 5.0 完成
让命令可以在全局执行
上面的开发完成之后,还要最后两步,才能让命令在全局运行。
-
在
index.js
的最上面,加入#!/usr/bin/env node
,这个称之为Shebang -
在
package.json
中加入以下节点:
"bin": {
"yginit": "./index.js"
}
发布到npm
使用npm login
登录之后,再使用npm publish
即可发布了。
至此,一个CLI就开发完成了。