JavaScript
Git
Node.js
GitHub

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

概要

前回、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に書くと例外を飛ばして死にます。流石にそれぐらいで死なれるといろいろ使いづらいのでエラーを吐かして例外は握りつぶします。

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の例外はstdoutstderrともにバイナリ形式になっているのでtoString()で読めるようにしておきます。また返り値は成功/失敗のbooleanにします。]

githubに秘密鍵を上げるな

ということで秘密鍵がgit管理にならないようにしようと思います。その前に秘密鍵のファイル形式は?RSA秘密鍵ならutf-8で読み出せるので中身を見て秘密鍵っぽかったら例外を投げるようにします。
ssh-keygen(openssl)で秘密鍵を作ると以下のようになります。

not_use_rsa(RSA秘密鍵)
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAyLPUDqwbMzmlNBh9QDCYWDpUeRmpMe75i6u/8xC0+nXI+ovQ
...(略)...
YeEZ1nuXLMgKPa9cPkdc/Gu7iukCAoX81KKw4agM2RyKMEuO1lq68WJkQKkP
-----END RSA PRIVATE KEY-----

PRIVATE KEYを検索する関数を作ります。

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はディレクトリ単位なのでファイルはみないようです。だからディレクトリを走査させました。

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

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

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も自動で走らせます。また、コメントにプリフィックスがあるといいらしいので自動付与にしてみた...がdebugとかは結局commiterしかわからないのでイマイチかもしれない、そもそもallでやろうというのが暴論なのかもしれない。

remoteとpush

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形式にしてぶち込みます。

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 addgithubを登録します。....remote addまで作る気力はなかった。
後は

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の詳細設定とかもできそうだけどまだ部品づくりの段階だと思います。