このブログエントリの続きです:
Github API を使う(1)


前回は Github API を使って外部アプリケーションから OAuth 認証を行ってアクセストークンと取得するところまでをサンプルコードと一緒に紹介しました。今回は取得したアクセストークンを使って、実際に API を実行する様子を紹介します。内容としては前回の続きとなるので、まずは前回の内容を理解しておいてください。

また今回もこのサンプルアプリケーションを使って説明します。ソースコードなどはこちらを参照してください:
https://github.com/dotnsf/github_oauth_sample


今回紹介するのは2つの API です:
(1)ログインしたユーザー自身の情報を取り出す
(2)ログインしたユーザーの権限で対象リポジトリ内の main ブランチのファイル一覧を取り出す


まずは(1)の実装を説明します。(1)は機能としては前回説明した内容にも含まれていました。前回の最後にサンプルアプリケーションにログインすると、ログインしたユーザーのアバター画像(URL)や Github ID を取得して表示していたのですが、実はここで既に(1)に相当する API が実行されていました:
2021051602


これはソースコードでは api/github.js 内の(前回紹介した)アクセストークンを取得した直後に GetMyInfo() 関数を実行して取得していたものです。この GetMyInfo() 関数は以下のようになっています:
async function GetMyInfo( access_token ){
  return new Promise( async function( resolve, reject ){
    if( access_token ){
      var option = {
        url: 'https://api.github.com/user',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        method: 'GET'
      };
      request( option, async function( err, res0, body ){
        if( err ){
          console.log( { err } );
          resolve( false );
        }else{
          body = JSON.parse( body );
          //. body = { login: 'dotnsf', id: XXXXXX, avatar_url: 'xxx', name: 'きむらけい', email: 'xxx@xxx', created_at: 'XX', updated_at: 'XX', ... }

          resolve( body );
        }
      });
    }else{
      resolve( false );
    }
  });
}

取得したアクセストークン(access_token)をヘッダに入れて GET https://api.gihub.com/user を実行しています。これで OAuth 認証したユーザーの情報を取得することができ、その結果(body)をそのまま返す、という関数です。

ログイン後のサンプルアプリケーションではこの結果を使ってアバターの URL (avatar_url)から Github アバター画像を表示したり、ホバー時に Github ID を表示するようにしていました:
2021051602


次に(2)です。これはログイン後の画面に表示される files ボタンをクリックすると画面下部に表示される、dotnsf/my_githubapi_test リポジトリ内 main ブランチのファイル一覧を取得する API です:
2021051604


ソースコードでは少し長めの関数(ListFiles())になっています。複数の Github API を連続で実行しているので、1つずつ説明します:
//. 現在のファイル一覧
async function ListFiles( access_token ){
  return new Promise( async function( resolve, reject ){
    if( access_token ){
      //. main ブランチの SHA 取得
      var option1 = {
        url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/main',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        method: 'GET'
      };
      console.log( { option1 } );
      request( option1, async function( err1, res1, body1 ){
        if( err1 ){
          console.log( { err1 } );
          resolve( false );
        }else{
          body1 = JSON.parse( body1 );
          console.log( { body1 } );  //. body1 = { message: 'Git Repository is empty.', documentation_url 'xxx' }  ->  あらかじめリポジトリの main ブランチに README.md などを登録しておくことで回避
          var sha1 = body1.object.sha;

          //. インスペクト
          var option2 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/commits/' + sha1,
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          request( option2, async function( err2, res2, body2 ){
            if( err2 ){
              console.log( { err2 } );
              resolve( false );
            }else{
              body2 = JSON.parse( body2 );  //. body2 = { commit: {}, url: '', author: {}, files: [], .. }
              console.log( { body2 } );

              //. tree
              var option3 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees/' + body2.sha,
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                method: 'GET'
              };
              request( option3, async function( err3, res3, body3 ){
                if( err3 ){
                  console.log( { err3 } );
                  resolve( false );
                }else{
                  body3 = JSON.parse( body3 );
                  console.log( { body3 } ); //. body3.tree = [ { path: "README.md", size: 130, url: "", .. }, .. ]
    
                  resolve( body3.tree );
                }
              });
            }
          });
        }
      });
    }else{
      resolve( false );
    }
  });
}

この ListFiles() 関数では Github API を3回実行しています。特定ブランチのファイル一覧を取得するだけなんですが、一発で取得するような便利な方法は用意されておらず、ある程度 Git の内部構造を理解する必要もあります。まあ、いい勉強にもなるので、もう少しお付き合いください。

ListFiles() 関数内では以下3つの Github API を順次実行しています:

(2-1)対象リポジトリの、対象ブランチの SHA 値を取得する

Github API では多くのタイミングで SHA(Secure Hash Algorithm) 値を取得したり、取得した値を使って実行したりします。これは1つのリポジトリ内に多くのブランチが存在していたり、ブランチ内にも多くのコミット情報が記録されているので、「リポジトリ内のある瞬間の状態」が複数管理されている状態にあるわけです。そのそれぞれの状態が SHA によるハッシュ値を使って管理されています。今回はファイル一覧の情報を取得することが目的なのですが、「どのファイル一覧」かを指定するための準備段階から実装する必要があります。

ソースコード(api/github.js)内の ListFiles() 関数では、まず(settings.js 内の repo_name 変数に記載された)対象リポジトリの main ブランチを対象としてファイル一覧を取得しています。その第一段階として現在の main ブランチの SHA を取得する必要があり、具体的には GET https://api.github.com/repos/(対象リポジトリ)/(対象ブランチ)/git/refs/heads/main を実行して、その実行結果の object.sha 値を取り出しています。


(2-2)対象ブランチの最新コミットの SHA 値を取得する

取得した main ブランチの SHA を使って、同ブランチの最新コミットの SHA 値を取得(インスペクト)します。ここは具体的には GET https://api.github.com/(対象リポジトリ)/(対象ブランチ)/commits/(SHA値) を実行し、その実行結果の sha 値を取り出しています。


(2-3)対象ブランチの最新コミットのファイルツリーを取得する

ここまでの準備作業を経て、目的のファイル一覧を取得できるようになります。ファイル一覧はツリーと呼ばれる構造に格納されているので、直前に取得したコミットの SHA 値を使って、GET https://api.github.com/(対象リポジトリ)/(対象ブランチ)/git/trees/(SHA 値) を実行し、その実行結果の tree 値を取り出して、関数の実行結果としています。


フロントエンド側ではこの結果を使ってファイル一覧を表示します:
2021051604



とりあえず2回に渡って Github API を使った OAuth ログインおよびアクセストークンの取得と、取得したアクセストークンを使っての(ログインしたユーザー権限での)Github API をいくつか実行する様子をサンプルを交えて紹介しました。基本的にはこの応用でもっといろいろな API も実行できるし、前回の冒頭で紹介した「Github リポジトリを使ったファイルサーバーっぽいもの」の開発もできると思っています。

予定としては次回のエントリで、実際に Github API で作成したファイルサーバーを紹介するつもりです。お楽しみに。


(2021/05/21 追記 続きはこちら