Node.js で Git 操作。simple-git を使ってみた

Node.js プログラムの中で Git 操作をする必要が出た。そのようなライブラリはいくつかあるようだが、simple-git というライブラリが手っ取り早そうだったので紹介する。

目次

前提条件

simple-git は内部的に child_process.spawn() を使って実際の git コマンドを実行する、ラッパーライブラリに過ぎない。

つまり、simple-git 含めた Node.js プログラムを動かす環境に、git コマンドがインストールされている必要がある。

グローバルな Git Config なども影響するので、動作環境に注意が必要だ。

とりあえず試してみる

それではとりあえず書いてみるとしよう。TypeScript ベースで書いてみるので、最低限のパッケージ類を用意する。

# 適当な作業ディレクトリを作成する
$ npm init -y

# tsconfig.json を準備する
cat <<EOL > tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs"
  }
}
EOL

$ npm i -S simple-git
$ npm i -D typescript ts-node @types/node

# コーディングするファイルを作っておく
$ touch index.ts

コレで準備 OK。一応、本稿執筆時点でのパッケージバージョンは次のとおり。

$ npm ls --depth=0
practice-simple-git@0.0.0 /Users/Neo/practice-simple-git
├── @types/node@14.0.14
├── simple-git@2.11.0
├── ts-node@8.10.2
└── typescript@3.9.6

index.ts を次のように実装していく。

import { promises as fs } from 'fs';
import simpleGit from 'simple-git';

(async () => {
  // Git リポジトリを配置するローカルのパス。フルパス指定
  const workingDirectory = `${__dirname}/my-pepo`;
  // 操作したい Git リポジトリの URL。必要に応じてユーザ名やパスワードを指定したりする
  const repositoryUrl = `https://github.com/Neos21/Neos21.git`;
  
  try {
    // 対象のディレクトリがなければ git clone する
    if(!await fs.access(workingDirectory).then(() => true).catch(() => false)) {
      await simpleGit().clone(repositoryUrl, workingDirectory);
    }
    
    const git = simpleGit(workingDirectory);
    
    // 指定のブランチに切り替える (以下はリモートブランチがない場合はエラーとなる書き方)
    await git.checkout('feat/example');
    await git.pull();
    
    // ファイルを保存したり〜
    const newFilePath = `${workingDirectory}/NEW-FILE.txt`;
    await fs.writeFile(newFilePath, 'New File Text', 'utf-8');
    
    // コミット・プッシュするなど
    await git.add(newFilePath);
    await git.commit('Example Commit');
    await git.push();
  }
  catch(error) {
    console.error('エラー', error);
  }
})();

こんな感じ。

最近知ったのだが、Node.js の fs モジュールに、Promise 化済のモジュールが存在していた。fs.promises というヤツで、生 Node.js だと

const fs = require('fs').promises;

なんて読み込んでも良い。もう util.promisify って使わなくて良いのね…。

んで、fs.exists() および fs.existsSync() は非推奨になっているので、存在チェックのために fs.access() を使用した。コレは Promise<void> なメソッドなので、Boolean に扱うために if 文の中でちょっとした工夫をしている。await した関数の Boolean を反転させる時は !await (! await) で良い。git clone については後述する。

Git 用のディレクトリが用意できたら、simpleGit(path) でインスタンスを作っておく。後はコレを使って git コマンド1つにつき asyncawait で処理してやれば良い。当然 Promise で書いても良い。

今回の例では、指定のブランチに切り替え → Pull で最新化 → 適当なファイルを作成する (ココは fs で普通に作業) → Add して Commit して Push、という流れ。こんなことが Node.js 上で出来てしまうのだ。

クレデンシャル情報の渡し方

simpleGit().clone()git clone を行うのだが、コレはターミナルで

$ git clone 【Git URL】

コマンドが実行されるのと同じことになる。つまり、~/.gitconfig などでクレデンシャル情報が辿れないと、そのリポジトリをクローンできないのだ。

GitHub なんかでもっとも手っ取り早いのは、URL にユーザ名とパスワード (or トークン) を混ぜ込んでしまう例。

const userName = 'Neos21';
const password = 'MY_PASSWORD';
await simpleGit().clone(`https://${userName}:${password}@github.com/USER_NAME/REPOSITORY_NAME.git`, workingDirectory);

環境変数なんかで注入できれば、コレが楽かもしれない。

Docker コンテナに閉じた環境であれば、~/.gitconfig および ~/.gitconfig-credential を用意して、

[credential]
    helper = store --file ~/.gitconfig-credential

みたいなグローバルコンフィグを用意しても良いだろう。

また、~/.netrc ファイルを用意して、

machine github.com
login 【ユーザ名】
password 【パスワード】

と記述して配置すれば、git clone 時の URL にユーザ名やパスワードを書かなくても処理できたりする。

いずれにおいても、生の git コマンドがそのマシン環境で実行されていることを見越して、Docker コンテナに閉じる際はどうやってクレデンシャル情報を渡しておこうか、といったことを考えておく必要がある。

以上

手軽に Git 操作ができて良き良き。