Bitbucket API を使って複数のリポジトリからファイルをダウンロードしてきて比較してみる

Bitbucket という、GitHub ライクでプライベートリポジトリが持てるホスティングサービスを使っている。

この Bitbucket にも API が用意されていて、今回やりたかったことに使えそうだったので、試しにやってみた。

目次

Bitbucket API

Bitbucket API はバージョン1系とバージョン2系があるらしいのだが、自分の環境は諸事情によりバージョン1系を使っているので、以下あたりの説明を見ていた。

コレによると、複数のリポジトリをまとめた「プロジェクト」は以下のような URL で取得できる。

この要領でリポジトリを絞り込み、さらにそのリポジトリ内を参照できるようだ。

こんな感じ。凄い。

リポジトリからファイルを取得する

今回、ワケあって複数のリポジトリにある package.json を比較調査したかったのだが、全てのリポジトリを git clone してきて、package.json だけコピーして作業フォルダに置いて…といった作業が面倒くさかった。

そこで、この Bitbucket API を利用して、各リポジトリからファイルを取得してみようと思う。

リポジトリ内のファイルを参照するには、以下のような URL になる。

コレだと master ブランチの最新のファイルしか取れないが、at パラメータでコミット ID かブランチ名を指定すれば、過去のファイルや別ブランチのファイルも取得できる。

今回 package.json は、リポジトリの直下にあったが、もし子ディレクトリにあるファイルを参照する場合は、…/browse/src/app/some-file.md というように、スラッシュ / を URL にそのまま付与して掘り下げられる。

at パラメータでブランチ名を指定する場合は、refs/heads/ を先頭に付け、記号などは encodeURIComponent() でエンコードしておく必要がある。だから /%2F に変換され、refs%2Fheads%2Fmaster となっている。

試しに curl でデータを取得してみる。Basic 認証は -u オプションで行えば良い。取得した JSON は lines プロパティに当該ファイルの行ごとのデータが入っているので、コレを jq でパースする。

$ curl -u '【ユーザ名】:【パスワード】' 'http://my-bitbucket.com/rest/api/1.0/projects/my_project/repos/my_repository/browse/package.json?at=refs%2Fheads%2Fmaster' | jq -r '.lines[].text'

よきよき。

複数のリポジトリからファイルを取得して保存・整形する

Bitbucket API の使い方が分かったので、あとはリポジトリごとの情報を控えておき、Bitbucket API を順に叩いては対象ファイルを書き出す Node.js スクリプトを書いてみる。

Node.js 組み込みの http モジュールを使っても良いけど、request というパッケージが便利なので、コレを使ってみた。

const request = require('request');
const fs = require('fs');
const os = require('os');

// 設定事項
const username = '【ユーザ名】';
const password = '【パスワード】';
const bitbucketURL = 'http://my-bitbucket.com';
// Basic 認証文字列
const basicAuth = `Basic ${new Buffer.from(`${username}:${password}`).toString('base64')}`;

// 取得したいデータを一覧にする
const dataList = [
  { project: 'my_project'    , repository: 'my_repository'    , branch: 'master'    , filePath: 'package.json' },
  { project: 'second_project', repository: 'second_repository', branch: 'develop'   , filePath: 'package.json' },
  { project: 'other_project' , repository: 'other_repository' , branch: 'feat/other', filePath: 'package.json' }
];

// 出力先ディレクトリ
const destDir = './package-json-files';

// ディレクトリを作る
if(!fs.existsSync(destDir)) {
  fs.mkdirSync(destDir);
}
// 順に取得 → ファイル保存を行う
dataList.reduce((promiseChain, current, index) => {
  return promiseChain
    .then(() => {
      return getFile(current.project, current.repository, current.branch, current.filePath);
    })
    .then((result) => {
      // ファイル名を作る : 今回は JSON ファイルを扱うのでココだけ拡張子ベタ書き…
      // ファイル終端の改行がなくなるので、os.EOL を使って改行コードを入れてみた
      const num = `0${index + 1}`.slice(-2);
      fs.writeFileSync(`${destDir}/${num}-${current.repository}.json`, `${result.fileData}${os.EOL}`);
    })
    .catch((error) => {
      console.error('Fail', error);
    });
}, Promise.resolve())
  .then(() => {
    console.log('Finished');
  });

/**
 * Bitbucket より指定のファイルを取得する
 * 
 * @param project プロジェクト名
 * @param repoository リポジトリ名
 * @param branch ブランチ名
 * @param filePath ファイルまでのフルパス
 * @return repository・branch・filePath および fileData プロパティとして取得したファイルの中身を返す
 */
function getFile(project, repository, branch, filePath) {
  return new Promise((resolve, reject) => {
    // at オプションでブランチを指定する・at オプションに渡す文字列は URI エンコード ('/' → '%2F' など) する
    request.get(`${bitbucketURL}/rest/api/1.0/projects/${project}/repos/${repository}/browse/${filePath}?at=${encodeURIComponent(`refs/heads/${branch}`)}`, {
      headers: {
        Authorization: basicAuth
      }
    }, (error, _response, body) => {
      if(error) {
        console.error('Request Error', error);
        return reject(error);
      }
      
      const parsedBody = JSON.parse(body);
      
      // API からエラーが返ってきている場合はココで判定する
      if(parsedBody.errors) {
        console.error('Bad Request', parsedBody.errors);
        return reject(parsedBody.errors);
      }
      
      // lines プロパティの中身のみをまとめ直す
      const fileData = parsedBody.lines.map((line) => {
        return line.text;
      }).join('\n');
      
      resolve({
        repository: repository,
        branch: branch,
        filePath: filePath,
        fileData: fileData
      });
    });
  });
}

定数 dataList に、リポジトリとブランチ名、ファイルパスを列挙していく。正常に動かせれば、./package-json-files/ ディレクトリ配下に

という名前で、各リポジトリの package.json が出力されていることだろう。

request パッケージで Basic 認証を行う方法は以下を参考にした。

headersAuthorization というキーを用意し、

const basicAuth = `Basic ${new Buffer.from(`${username}:${password}`).toString('base64')}`;

こんな書式で認証用の文字列を用意する。

ファイルを Bitbucket API から取得すると、ファイル終端の改行コードがなくなるので、os.EOL を使ってファイル書き出し時に改行コードを付与してみた。この辺雑だけど動いてるからいいや…。

コレでファイルをかき集めることができたので、あとはよしなに整形して利用すれば良い。Bitbucket API 便利!