JavaScript
Node.js
Subversion
svn-spawn

Node.jsでSVNからcheckoutするためにsvn-spawnを使う

コード

checkout.js
const Client = require('svn-spawn');
const client = new Client({
    // ディレクトリがなければ作成される
    cwd: __dirname + '/../target',
    // 実行時に手動入力できるのでコードに載せなくても良い。
    //username: 'username', // optional if authentication not required or is already saved
    //password: 'password', // optional if authentication not required or is already saved
    noAuthCache: true, // optional, if true, username does not become the logged in user on the machine
});

// targetディレクトリは作成されないので、cwdの方に新規ディレクトリを指定しないと混ざるので注意
client.checkout('http://repo/~/target');

実行すると、

> node .\checkout.js
Authentication realm: <http://repo:80> Kerberos Login
Password for 'khsk': ********
A    hogehoge.txt
~~

Checked out revision xxxx.

とパスワードを聞いてくるので入力してやる。
ユーザー名はたぶん普段遣いのSVNの記録から読み込んでいると思われる。パスワードを間違えればUsernameからの入力になるので、違うユーザーでチェックアウトしたいときも焦らない。

実行環境

  • Windows7
  • node v9.3.0

ライブラリ

Readmeにはcheckoutがなくて、client.cmdからか…と凹んだが、念の為コードを検索すると

node-svn-spawn/svn.js at 59c4295e33403023f4e8a0e1fe34447a1cef9b5b · ddliu/node-svn-spawn

ちゃんと生えていた。

(peteward44/node-svn-ultimate: The ultimate SVN wrapper for node. Contains all the basic methods checkout, update, info, etc, and includes svnmucc supportを先に試したんですが、失敗したんですよね)

Promise化したい

await大好きマンなので、コールバックはちょっと…

コールバック関数をPromise化して使う - Qiita
いいのがあった。

promise1.js
const Client = require('svn-spawn');
const {promisify} = require('util');

const client = new Client({
    // ディレクトリがなければ作成される
    cwd: __dirname + '/../target',
    // 実行時に手動入力できるのでコードに載せなくても良い。
    //username: 'username', // optional if authentication not required or is already saved
    //password: 'password', // optional if authentication not required or is already saved
    noAuthCache: true, // optional, if true, username does not become the logged in user on the machine
});

// targetディレクトリは作成されないので、cwdの方に新規ディレクトリを指定しないと混ざるので注意
(async () => {
    await promisify(client.checkout)('http://repo/~/target');
})();
promise1実行結果
(node:7396) UnhandledPromiseRejectionWarning:TypeError: Cannot read property 'getOption' of undefin
ed    at Client.checkout (~\node_modules\svn-spawn\lib\svn.js:4
0:20)

…

4行目ではないけれど
node-svn-spawn/svn.js at 59c4295e33403023f4e8a0e1fe34447a1cef9b5b · ddliu/node-svn-spawn
このthis.getOptionが問題な気がする。(util.inherits初めてみた。継承らしい。)
thisの値が変わっちゃったのかな?

thisclientに戻してやればいいのかなーと.bind(client)の位置をあれこれいじり、

promise2.js
const Client = require('svn-spawn');
const {promisify} = require('util');

const client = new Client({
    // ディレクトリがなければ作成される
    cwd: __dirname + '/../target',
    // 実行時に手動入力できるのでコードに載せなくても良い。
    //username: 'username', // optional if authentication not required or is already saved
    //password: 'password', // optional if authentication not required or is already saved
    noAuthCache: true, // optional, if true, username does not become the logged in user on the machine
});

// targetディレクトリは作成されないので、cwdの方に新規ディレクトリを指定しないと混ざるので注意
(async () => {
    await promisify(client.checkout).bind(client)('http://repo/~/target');
})();

とした。
が、どうやらコールバックのときから、チェックアウトには成功するものの失敗扱いらしく、Rejectされるのでtry/catchしてやらないといけない。1
ついでにakamecoさん風ならawait/catch
というか今回は

  • 同期処理(await)が目的
  • 成功がエラーなので握りつぶす(邪悪)

なので、await/catchの方が簡潔に握りつぶせると気づいた。

async関数においてtry/catchではなくawait/catchパターンを活用する - Qiita
(ちよ父すき)

つまり

(async () => {
    try {
        await promisify(client.checkout).bind(client)('http://repo/~/target');
    } catch () {}
})();

// よりも

(async () => {
    await promisify(client.checkout).bind(client)('http://repo/~/target').catch(() => {});
})();

ネストも行数も減るし見やすい。
(ちゃんとしたtry/catchの場合は上下どちらにももう一段try/catchが必要かな。上のtry/catchは握りつぶすためのtry/catchなので)

これでひとまず、チェックアウト後の処理をコールバックの中に書かずに済むようになった。

やっぱりexportも

同じフォルダにSVNから別のファイルを参照用に置きたくなった。
となると同一ディレクトリに別リポジトリはおけないので、エクスポートすることに。
exportは残念ながら実装されていないみたい
Issues · ddliu/node-svn-spawn
なので、結局コマンドで実行することに。

await promisify(client.cmd).bind(client)(['export','http://repo/~/targetfile']).catch(() => {});

と続けて書いてエクスポートした。
clientを使いまわしているので、ダウンロード先は同じcwdになる。


  1. 普段から真面目にtry/catchしてる人なら出ない発言