Nodeでgitのラッパーを書く(2)---add, commit, remote

# 概要
[前回](https://qiita.com/forl-developer/items/df2e4b8a37e3c4359774)、gitのbranch,ls-file,log,status等の情報収集系のnode.jsでラップしたユーティリティを作った。
次に本格的にgitを使えるようにするためにadd,commitを使えるようにする。また、gitといえばgithubなのでremoteとpushを使えるようにしてgithubにも上げられるようにする。

## 作る前にいろいろ確認
### .gitignore
gitは`.gitignore`でgitで管理**しない**ファイルを指定するが自分の作業ディレクトリが汚すぎるので少し普通ではない書き方をする。

```:.gitignore
*  # すべてのファイルを除外
*~  # 一時バックアップを除外
#*# # emacsのバックアップファイルの除外
*.swap # vi系のバックアップファイルの除外
!.gitignore # .gitignoreはどこにあっても無視しない
!/git/
!/git.js
!/package.json
```
```:git/.gitignore
!* # git/ディレクトリ以下はすべて解除
*~
#*#
*.swp
```
とルートディレクトリは'*'で全ファイル無視をして特定の者だけ解除します。サブディレクトリですべて解除しないとディレクトリのみの解除ですが空ディレクトリになってaddされなくなります。

### addコマンド
.gitignore無視されるファイルをaddしようとするとコマンドエラーになります。そのままnodeに書くと例外を飛ばして死にます。流石にそれぐらいで死なれるといろいろ使いづらいのでエラーを吐かして例外は握りつぶします。

```js:git/add/file.js
const exec=require('child_process').execSync;

const file=(filepath)=>{
    if( filepath==null ) return false;
    try{ exec('git add '+filepath); }
    catch(e){
        console.log('git.add.file catch exception');
        if( e.stdout.toString().length>0 ){ console.log('std out : ', e.stdout.toString()); }
        console.log('std err : ', e.stderr.toString());
        return false;
    }
    return true;
}

module.exports=file;
```
execSyncの例外は`stdout``stderr`ともにバイナリ形式になっているので`toString()`で読めるようにしておきます。また返り値は成功/失敗のbooleanにします。]

### [githubに秘密鍵を上げるな](https://qiita.com/saitotak/items/813ac6c2057ac64d5fef)
ということで秘密鍵がgit管理にならないようにしようと思います。その前に秘密鍵のファイル形式は?[RSA秘密鍵ならutf-8で読み出せる](http://bearmini.hatenablog.com/entry/2014/02/05/143510)ので中身を見て秘密鍵っぽかったら例外を投げるようにします。
`ssh-keygen`(openssl)で秘密鍵を作ると以下のようになります。

```:not_use_rsa(RSA秘密鍵)
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAyLPUDqwbMzmlNBh9QDCYWDpUeRmpMe75i6u/8xC0+nXI+ovQ
...(略)...
YeEZ1nuXLMgKPa9cPkdc/Gu7iukCAoX81KKw4agM2RyKMEuO1lq68WJkQKkP
-----END RSA PRIVATE KEY-----
```
`PRIVATE KEY`を検索する関数を作ります。

```js:isPrivateKey.js
const fs=require('fs');

const isPrivateKey=(filepath)=>{
    if( !fs.existsSync(filepath) ) return false;
    if( fs.statSync(filepath).isDirectory() ) return false;

    return fs.readFileSync(filepath, 'utf-8').includes('RSA PRIVATE KEY');
}

module.exports=isPrivateKey
```

これだと例外を投げませんでした。addはディレクトリ単位なのでファイルはみないようです。だからディレクトリを走査させました。

```js:includesPricateKey.js
const fs=require('fs'), path=require('path');
const isPrivateKey=require('./isPrivateKey');

const includesPrivateKey=(filepath)=>{
    if( !fs.existsSync(filepath) ) return false;
    if( fs.statSync(filepath).isDirectory() ){
        const files=fs.readdirSync(filepath);
        for( const f of files ){
            if( fs.statSync(path.join(filepath, f)).isDirectory() ) continue;
            if( isPrivateKey(path.join(filepath, f)) ) return true;
        }
        return false;
    }

    return isPrivateKey(filepath);
}

module.exports=includesPrivateKey
```
`readFileSync`はディレクトリに対して行うと例外を出すので回避させます。`fs.readdirSync`はその階層のファイル(ディレクトリ)名のみを返すので`path`を使ってつなげます。(OSの互換性を保つには文字列操作でくっつけないほうがいい)とこれで前に作った秘密鍵がaddの段階で発見されました。
今回はディレクトリ階層は掘りませんでしたが再帰を使うと簡単に掘れるのでgithubに秘密鍵を上げると抜かれるのも納得です(パート.1で紹介したshelljsでも簡単に抜けそうだし、wgetとfindとgrepがあれば多分、抜けます)。

## add
```js:git/add/all.js
const status=require('../status');
const file=require('./file');

const all=()=>{
    const untracked=status.untracked();

    const arr=[];
    for( const a of untracked ){ if( file(a) ) arr.push(a); }
    return arr;
}

module.exports=all;
```
`add .`に相当する(?)コマンド、前回作った未追跡ファイルをaddするようにした。add自体は`git.add.file`に投げているので秘密鍵問題はallを使っても発見されます。

## commit
```js:git/commit/all.js
const exec=require('child_process').execSync;
const status=require('../status.js');
const add=require('../add.js');

const all=(message)=>{
    const modified=status.modified();
    const untracked=status.untracked();

    for( const a of untracked ) add.file(a);

    let msg='';
    if( modified.length>0 ) msg+='[modify]'
    if( untracked.length>0 ) msg+='[add]'
    if( msg.length===0 ){
        console.log('git.commit no updated file');
        return '';
    }
    if( message!=null ) msg+=' '+message;
    else msg+=' auto message by gitjs';
    const out=exec("git commit -a -m '"+msg+"'").toString();
    return out;
}

module.exports=all;
```
すべてをコミットするコマンドです。前に書いた`add`も自動で走らせます。また、コメントに[プリフィックスがあるといいらしい](https://qiita.com/itosho/items/9565c6ad2ffc24c09364)ので自動付与にしてみた...が`debug`とかは結局commiterしかわからないのでイマイチかもしれない、そもそもallでやろうというのが暴論なのかもしれない。

## remoteとpush
```js:git/remote/json.js
const exec=require('child_process').execSync;

const json=()=>{
    const out=exec('git remote -v').toString();
    const lines=out.split('\n').filter(a=>{ return a.length!==0 });
    const arr=[];
    for( const l of lines ){
        const [ a, bc ] = l.split('\t');
        const [ b, c ]=bc.split(' ');
        arr.push([ a, b, c.replace('(', '').replace(')', '') ]);
    }

    const json={};
    for( const l of arr ){
        if( json[l[0]]==null ) json[l[0]]={};
        if( json[l[0]]['options']==null ) json[l[0]]['options']=[ l[2] ];
        else json[l[0]]['options'].push(l[2]);
        json[l[0]]['url']=l[1];
    }
    return json;
}

module.exports=json;
```
remoteも情報を取ってくるだけなので全てjson形式にしてぶち込みます。

```js:git/push
const exec=require('child_process').execSync;

const optParse=(json)=>{
    let opt='';
    for( const [ key, val ] of Object.entries(json) ){
        if( typeof val==='boolean' ) opt+='-'+key+' ';
        else opt='-'+key+'='+val;
    }
    return opt;
}

const argsParse=(...args)=>{
    if( args.length===1 && Array.isArray(args[0]) && args[0].length==2 && typeof args[0][0]==='string' && typeof args[0][1]==='string' ){
        return [ '', args[0][0], args[0][1] ];
    }
    if( args.length===2 ){
        if( typeof args[0]==='object' && Array.isArray(args[1]) ) return [ optParse(args[0]), args[1][0], args[1][1] ];
        if( typeof args[1]==='object' && Array.isArray(args[0]) ) return [ optParse(args[1]), args[0][0], args[0][1] ];
        if( typeof args[0]==='string' && Array.isArray(args[1]) ) return [ args[0], args[1][0], args[1][1] ];
        if( typeof args[1]==='string' && Array.isArray(args[0]) ) return [ args[1], args[0][0], args[0][1] ];
    }
    throw new Error('!!!!! git.push invailed arguments!!!!!');
}

const push=(...args)=>{
    const [ opt, remote, local ]=argsParse(...args);
    console.log('git push '+opt+' '+remote+' '+local);
    return exec('git push '+opt+' '+remote+' '+local).toString();
}

module.exports=push;
```
pushはリモートとローカルブランチ、オプションもあったりなかったりなので引数化しました。リモートローカルは長さ2の配列、オプションは`string`もしくは`json`形式にしています。

# パート2の最後に
前に言っていたようにgithubに上げます。アカウントの取得とリポジトリの作成はやっておき`git remote add`で`github`を登録します。....remote addまで作る気力はなかった。
後は

```js:gitjs/samples/updateGitHub.js
const git=require('git');

console.log(git.commit.all());
console.log(git.push({ u: true }, [ 'github', 'master' ]));
```
を実行します。これでadd、commit、pushが行われます。
gitjs---https://github.com/forl-developer/gitjs
にあります。

# 足りないものとか
commitがallだけだと最終的に使いづらくなりそう配列形式でのコミットとかmodifiedファイルのみのコミットとか消されたファイル分だけ分けてコミットとか。後はreset、reflogあたりのいざというときコマンド等の追加でしょうか。
webpackライクなgit.config.jsを使ったgitの詳細設定とかもできそうだけどまだ部品づくりの段階だと思います。