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

過去2つのエントリで Github API の基本的な内容として OAuth による認証を行ってアクセストークンを取得し、そのアクセストークンを使って(ログインしたユーザーの権限で)「自分自身の情報を取得する」、「特定リポジトリの特定ブランチに含まれるファイル一覧を取得する」という2つの Github API の実装内容をサンプルソースコードと合わせて紹介しました。

シリーズ最終回の今回はこれまでの内容に加えて以下の機能を Github API で実装して、このシリーズ当初の目的であった「Github ベースのファイルサーバー」っぽいものを作ってみます:
・ブランチを作成する/マージする
・ブランチにファイルをコミットする


実際に作ってみたものを先に紹介しておきます。ソースコードはこちらから参照してください:
https://github.com/dotnsf/githubapi

2021051901


これまでのサンプルと同様に git clone などでソースコード取得後、 settings.js に OAuth アプリケーションの client_id と client_secret 、コールバック URL、API で操作する対象のリポジトリを指定します(この時に作って登録した OAuth アプリケーションのものを流用しても構いません)。またこれも同様ですが、アプリケーション登録時に使ったユーザーとは別のユーザーでアクセスする場合は collaborators に追加しておく必要があります。

加えて、このアプリケーションでは各ユーザー毎にブランチを(ログイン時に自動的に)作成してもらい、そのブランチで作業した内容を特定の別ブランチにマージできるような機能があり、そのマージ先のブランチを target_branch_name 変数に指定する必要があります(デフォルト状態では "__all__")。この初期設定を最初に済ませておいてください。

ここで早速動作確認、、、する前に対象となるリポジトリがアプリケーションによってどのように変化していくかを確認しておきます。今回 settings.js の repo_name で指定されたリポジトリはこちらで、main ブランチの中に README.md ファイルが1つだけ格納されている状態です:
2021051905


また main 以外にはブランチは存在していません:
2021051906


ではこのリポジトリがどのように変わっていくのかを確認するため、改めてアプリケーションを実行します:
$ npm install

$ node app

起動したらウェブブラウザで http://localhost:8080/ にアクセスして、前回までと同様に画面右上の login ボタンから Gihub ユーザー ID でログインします:
2021051902


OAuth ログインに成功すると以下のような画面になります:
2021051903


画面右上にログインしたユーザーのアバター画像、画面内には files, merge, push という3つのボタンと、ファイル選択フィールドが表示されているはずです。各ボタンやフィールドの説明をする前に、この(ログイン直後の)時点でリポジトリ側に変化が加えられているので説明しておきます。

対象リポジトリをリロードして、改めてブランチを確認します。すると直前までは main 1つだけだったブランチに2つのブランチ("3183150" と "__all__")が追加されていることがわかります:
2021051907


このアプリケーションではユーザーがログインすると同時に Gihub ユーザー ID と同じ名前のブランチと、settings.js の target_branch_name 変数で指定したブランチの2つが指定リポジトリ内に作成されます(作成済みの場合は失敗するだけなので変化ありません)。この時点で指定リポジトリにはもともと存在していた main ブランチに加えて、ログインしたユーザーのユーザー ID 名と同じブランチ(以降、「個人ブランチ」と呼びます)と、(今回は "__all__" が指定されているので)__all__ ブランチが作成されています。つまり "318350" とはログインした私の Github ユーザー ID ということです。ちなみにユーザー ID を確認するにはログイン後にアバター画像の上をマウスでホバーした時に表示されます:
2021051904
(↑この例だと 318350)


別の(collaborators として権限を与えられた)ユーザーがログインすると、そのユーザーの個人ブランチも作成されます。なお新たに作成される個人ブランチは「その時点での main ブランチの内容をコピー」した状態で作成されます。

改めてボタン類の説明をします。このうち files ボタンは前回紹介したものと同様ですが、main ブランチではなく個人ブランチ内のファイル一覧が表示されます。ログイン直後は main ブランチをコピーした状態なので、もとの main ブランチに含まれていたファイルが一覧(この例では README.md)表示されます。ファイルの(個人ブランチからの)削除もここから行うことができます:
2021051908


まずこの状態に変化を加えるため、ファイルを1つ追加してみます。「ファイルを選択」と書かれた部分をクリックしてローカルシステムからファイルを1つ選択し、push ボタンをクリックします:
2021052001


すると選択したファイルがアップロードされ、個人ブランチに追加されます。アップロード後に files ボタンをクリックすると、アップロードしたファイルが追加されて一覧表示されることがわかります:
2021052002


今回のサンプルアプリでは DELETE ボタンでブランチから削除、またファイル名部分のクリックでファイルダウンロードまでが実装されています:
2021052003


またこの時点でリモートリポジトリの該当ブランチを確認すると、アップロードしたファイルが含まれていることを確認できます:
2021052004


ではこの個人ブランチに加えた変更(ファイル追加)を(settings.js で指定した)別ブランチにマージします。マージするには画面内の merge ボタンをクリックするだけです(画面に { "result": true } と表示されれば成功です):
2021052005


改めてリモートリポジトリの __all__ ブランチ(settings.js の target_branch_name 変数で指定したブランチ)を確認すると、このアクションによって個人ブランチでの変更がマージされているはずです。つまりマージ処理も Github API で実装できていることが確認できます:
2021052006


可能であれば(リポジトリに collaborators として招待した)別の Github ユーザーで同じ手順を実行してみてください。まずはログインして、別のファイルを push します:
2021052007


この時点で(正確には最初のログイン直後の時点で)このユーザーの個人ブランチが作られ、同ブランチにファイルが push された状態になっています:
2021052008

2021052009


ここでこのユーザーも merge を行うと、__all__ ブランチには元のユーザーが追加したファイルと、このユーザーが追加したファイル両方が含まれる状態になります(マージ元である個人ブランチには変化はありません):
2021052010


というわけで、ログインユーザー毎にブランチを作ってファイルを追加/削除し、特定ブランチにマージする機能が Github API で実装できました。当初「Github API を使ってファイルサーバーを作る」ことを目的としていたのですが、ここまで実現できたことで技術的に最低限必要な機能は実装できそうだと思っています。


【Github API による実装内容】
次にこのサンプルアプリケーションのソースコードを解説して、このアプリケーションの機能を Github API でどのように実装しているのかを紹介します。改めてソースコードはこちらです:
https://github.com/dotnsf/githubapi

また、以下の説明の大半は api/github.js ファイル内で実装している内容です。実際のソースコードを参照する場合はこのファイルを見て確認してください。

すべてのコードを説明するのはさすがにちと大変そうなので、「ブランチを作成」する処理と「ファイルをブランチに追加」するという2つの処理内容をソースコード含めて紹介します。


「ブランチ作成」
まずは「ブランチを作る」部分(正確には「main ブランチをコピーして新しいブランチを作る」部分)です。サンプルアプリケーションではログイン時に個人ブランチとマージ対象ブランチを作っています(いずれも作成成功するのは初回のみで、2回目以降は "already exists" というエラーになりますが無視します)。

OAuth 処理の一環で GET /api/callback を処理してログインが成功すると、InitMyBranch() 関数を実行します。この InitMyBranch() 関数は次のようになっています:
async function InitMyBranch( 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', ... }
          loggedIns[access_token].user = body;

          //. https://qiita.com/nysalor/items/68d2463bcd0bb24cf69b

          //. 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 data2 = {
                ref: 'refs/heads/' + body.id,
                sha: sha1
              };
              var option2 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                json: data2,
                method: 'POST'
              };
              request( option2, async function( err2, res2, body2 ){
                if( err2 ){
                  console.log( { err2 } );
                  resolve( false );
                }else{
                  console.log( { body2 } );  //. { message: 'Reference already exists', .. }
                  //body2 = JSON.parse( body2 );
                  //console.log( { body2 } );

                  //. 作成したブランチの SHA 取得(?)
                  var option3 = {
                    url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/' + body.id,
                    headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                    method: 'GET'
                  };
                  console.log( { option3 } );
                  request( option3, async function( err3, res3, body3 ){
                    var obj = false;
                    if( err3 ){
                      console.log( { err3 } );
                      //resolve( false );
                    }else{
                      body3 = JSON.parse( body3 );
                      console.log( { body3 } );  //. 権限がないユーザーだと { message: 'Not Found', documentation_url: '' }
                      if( body3.message ){
                        //resolve( false );
                      }else{
                        var sha3 = body3.object.sha;
                        //req.session.oauth.sha = sha3;
                        body.sha = sha3;

                        //. ターゲットブランチの生成結果に関係なく、この値を返す
                        obj = JSON.parse( JSON.stringify( body ) );
                        //resolve( body );
                      }
                    }

                    //. ターゲットブランチ作成
                    var data4 = {
                      ref: 'refs/heads/' + settings.target_branch_name,
                      sha: sha1
                    };
                    var option4 = {
                      url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                      headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                      json: data4,
                      method: 'POST'
                    };
                    request( option4, async function( err4, res4, body4 ){
                      if( err4 ){
                        console.log( { err4 } );
                      }else{
                        console.log( { body4 } );  //. { message: 'Reference already exists', .. }
                      }
                      resolve( obj );
                    });
                  });
                }
              });
            }
          });
        }
      });
    }else{
      resolve( false );
    }
  });
}

InitMyBranch 関数は OAuth 処理で取得したアクセストークンを引数に渡されて実行します。この関数内ではまず(1)個人ブランチを作成 して、次に(2)マージ対象ブランチを作成 します。処理内容は似ていますが、この順に説明します。

まず(1)個人ブランチの作成です。個人ブランチはユーザー個人の Github ユーザー ID 名のブランチを作ることになるので、まずは実行ユーザーの Github ユーザー ID を取得する必要があります。そのため以前のブログエントリでも説明した個人ユーザー情報を取得する API を実行します:
            :
            :
      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 );
            :
            :

これで Github API を取得すれば、作成する個人ブランチの名称が決まったことになります。次にその名称のブランチを作成するのですが、git のブランチは作成元ブランチを指定する必要があります(CLI でも同様ですが、ブランチ作成元になるブランチを指定する必要がある、という意味です)。今回は main ブランチを元に個人ブランチを作るので、まずは main ブランチの SHA 情報を取得しておく必要があります。というわけで、これも以前のブログエントリで説明した方法で main ブランチの情報を取得する API を実行します:
            :
            :
          //. 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 } );
              var sha1 = body1.object.sha;

            :
            :

これで個人ブランチを作成するために必要な情報が揃いました。あらためて main ブランチの SHA を指定して作成元を明示した上で、新しいブランチを Github ユーザー ID 名で作成します:
            :
            :
              //. 個人ブランチ作成
              var data2 = {
                ref: 'refs/heads/' + body.id,
                sha: sha1
              };
              var option2 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                json: data2,
                method: 'POST'
              };
              request( option2, async function( err2, res2, body2 ){
                if( err2 ){
                  console.log( { err2 } );
                  resolve( false );
                }else{
                  console.log( { body2 } );  //. { message: 'Reference already exists', .. }

            :
            :

余談ですが、既に個人ブランチが作成済みであった場合、この API 実行は失敗するのですが、REST API の HTTP ステータスとしては 200 が、作成メッセージとして "Reference already exists" が返ります(上のコードの赤字部分)。一般的にはこういう場合は 400 番代のエラーステータスコードが返ってくることが多いと思っていますが、この点は注意が必要です(処理的にはエラー扱いしなくてよい、こちらの方が楽ですけど・・)。

これで個人ブランチが作成された状態になりました。続いて同様にマージ対象ブランチを作成します。これも main ブランチを元に新しいブランチを作る形になるため、main ブランチの SHA を指定して、settings.js 内の target_branch_name 変数で定義した新しいブランチを作成します:
            :
            :
                    //. ターゲットブランチ作成
                    var data4 = {
                      ref: 'refs/heads/' + settings.target_branch_name,
                      sha: sha1
                    };
                    var option4 = {
                      url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                      headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                      json: data4,
                      method: 'POST'
                    };
                    request( option4, async function( err4, res4, body4 ){
                      if( err4 ){
                        console.log( { err4 } );
                      }else{
                        console.log( { body4 } );  //. { message: 'Reference already exists', .. }
                      }
                      resolve( obj );
                    });
            :
            :


「ファイルを追加」
次は「ブランチにファイルを追加する」部分です。処理としてはブラウザからファイルをアップロードして、そのアップロードしたファイルを API でブランチ(の tree)に追加する、という流れになります。tree については前回のブログエントリを参照してください。

まずはファイルアップロード処理の部分から見てみます。ブラウザ画面からファイルを選択して push ボタンを押すと、POST /api/file という API が実行されます。その内容がこちらで、アップロードされたファイル(のパス)を取り出し、アクセストークンなどと一緒に PushToMyBranch() 関数を実行しています:
router.post( '/file', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  if( req.session && req.session.oauth && req.session.oauth.token && req.session.oauth.id, req.session.oauth.sha ){
    //. https://qiita.com/ngs/items/34e51186a485c705ffdb
    var filepath = req.file.path;
    var filetype = req.file.mimetype;
    //var filesize = req.file.size;
    var ext = filetype.split( "/" )[1];
    var filename = req.file.filename;
    var originalfilename = req.file.originalname;

    var r = await PushToMyBranch( req.session.oauth.token, req.session.oauth.id, req.session.oauth.sha, filepath, filetype, originalfilename );
    console.log( { r } );
    if( r ){
      //. 追加したブランチの最新 SHA を取得
      r = await InitMyBranch( req.session.oauth.token );
      console.log( { r } );
      if( r ){
        req.session.oauth.sha = r.sha;
      }
    }

    //res.write( JSON.stringify( { result: r }, null, 2 ) );
    //res.end();
    res.redirect( '/' );
  }else{
    //res.status( 400 );
    //res.write( JSON.stringify( { error: 'no access_token' }, null, 2 ) );
    //res.end();
    res.redirect( '/' );
  }
});

PushToMyBranch() 関数内の処理がこちらです。
async function PushToMyBranch( access_token, id, sha, filepath, filetype, originalfilename ){
  return new Promise( async function( resolve, reject ){
    if( access_token && id && sha ){
      var data1 = {};
      if( filetype.startsWith( 'text' ) ){
        //. text
        var text = fs.readFileSync( filepath, 'utf8' );
        data1 = {
          content: text,
          encoding: 'utf-8'
        };
      }else{
        //. binary
        var bin = fs.readFileSync( filepath );
        data1 = {
          content: new Buffer( bin ).toString( 'base64' ),
          encoding: 'base64'
        };
      }
      console.log( { data1 } );
  
      //. BLOB 作成
      var option1 = {
        url: 'https://api.github.com/repos/' + settings.repo_name + '/git/blobs',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        json: data1,
        method: 'POST'
      };
      request( option1, async function( err1, res1, body1 ){
        if( err1 ){
          console.log( { err1 } );
          fs.unlink( filepath, function( e ){} );
          resolve( false );
        }else{
          //body1 = JSON.parse( body1 );
          //. body1 = { url: 'XXXXX', sha: 'XXXXXX' }
          console.log( { body1 } );
          var sha1 = body1.sha;

          //. ここで Tree を新規に作成するのではなく、既存の最新 Tree を取得して追加する
          //. 最後に InitMyBranch() を実行するなりして、セッション内 sha の更新が必要?
          //. インスペクト
          var option2 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/commits/' + sha,
            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: "", .. }, .. ]
    
                  //. Tree 追加
                  var data4 = { tree: [] };
                  data4.tree = JSON.parse( JSON.stringify( body3.tree ) );
                  data4.tree.push( { path: originalfilename, mode: '100644', type: 'blob', sha: sha1 } );

                  var option4 = {
                    url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees',
                    headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                    json: data4,
                    method: 'POST'
                  };
                  request( option4, async function( err4, res4, body4 ){
                    if( err4 ){
                      console.log( { err4 } );
                      fs.unlink( filepath, function( e ){} );
                      resolve( false );
                    }else{
                      //body4 = JSON.parse( body4 );
                      console.log( { body4 } );
                      var sha4 = body4.sha;

                      //. 現在の Commit の SHA を取得
                      var option5 = {
                        url: 'https://api.github.com/repos/' + settings.repo_name + '/branches/' + id,
                        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                        method: 'GET'
                      };
                      request( option5, async function( err5, res5, body5 ){
                        if( err5 ){
                          console.log( { err5 } );
                          fs.unlink( filepath, function( e ){} );
                          resolve( false );
                        }else{
                          body5 = JSON.parse( body5 );
                          console.log( { body5 } );
                          var sha5 = body5.commit.sha;

                          //. Commit を作成
                          var ts = ( new Date() ).getTime();
                          var data6 = {
                            message: '' + ts,
                            tree: sha4,
                            parents: [ sha5 ]
                          };
                          var option6 = {
                            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/commits',
                            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                            json: data6,
                            method: 'POST'
                          };
                          request( option6, async function( err6, res6, body6 ){
                            if( err6 ){
                              console.log( { err6 } );
                              fs.unlink( filepath, function( e ){} );
                              resolve( false );
                            }else{
                              //body6 = JSON.parse( body6 );
                              console.log( { body6 } );
                              var sha6 = body6.sha;
          
                              //. リファレンスを更新
                              var data7 = {
                                force: false,
                                sha: sha6
                              };
                              var option7 = {
                                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/' + id,
                                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                                json: data7,
                                method: 'PATCH'
                              };
                              request( option7, async function( err7, res7, body7 ){
                                if( err7 ){
                                  console.log( { err7 } );
                                  fs.unlink( filepath, function( e ){} );
                                  resolve( false );
                                }else{
                                  //body7 = JSON.parse( body7 );
                                  console.log( { body7 } );
                                  fs.unlink( filepath, function( e ){} );

                                  var sha7 = body7.object.sha;

                                  resolve( { sha: sha7 } );
                                }
                              });
                            }
                          });
                        }
                      });
                    }
                  });
                }
              });
            }
          });
        }
      });
    }else{
      if( filepath ){
        fs.unlink( filepath, function( e ){} );
      }
      resolve( false );
    }
  });
}

では PushToMyBranch() 関数内の処理を順に説明します。この関数内ではまず(1)ファイルの blob を作成 した後に(2)ブランチ内の現在のファイルツリーを取得 します。そして(3)ファイルの blob をツリーに追加 してから(4)ファイルをコミット し、最後に(5)リファレンス SHA を更新 する、という一連の流れを処理しています。では上述のコードを少しずつ見ながら、この流れを説明します。

まず(1)ファイルの blob を作成する部分です。ここはアップロードされたファイルのタイプを参照して、テキストファイルだったら UTF-8 エンコードで、それ以外だったら Base64 エンコードして中身を取り出した上で blob オブジェクトを作成し、その SHA を取り出します:
            :
            :
      var data1 = {};
      if( filetype.startsWith( 'text' ) ){
        //. text
        var text = fs.readFileSync( filepath, 'utf8' );
        data1 = {
          content: text,
          encoding: 'utf-8'
        };
      }else{
        //. binary
        var bin = fs.readFileSync( filepath );
        data1 = {
          content: new Buffer( bin ).toString( 'base64' ),
          encoding: 'base64'
        };
      }
      console.log( { data1 } );
  
      //. BLOB 作成
      var option1 = {
        url: 'https://api.github.com/repos/' + settings.repo_name + '/git/blobs',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        json: data1,
        method: 'POST'
      };
      request( option1, async function( err1, res1, body1 ){
        if( err1 ){
          console.log( { err1 } );
          fs.unlink( filepath, function( e ){} );
          resolve( false );
        }else{
          //body1 = JSON.parse( body1 );
          //. body1 = { url: 'XXXXX', sha: 'XXXXXX' }
          console.log( { body1 } );
          var sha1 = body1.sha;
            :
            :

(2)この blob をブランチに追加するのですが、そのためにブランチの現在のファイルツリーを取得しておきます:
            :
            :
          //. インスペクト
          var option2 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/commits/' + sha,
            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: "", .. }, .. ]
            :
            :

そして(3)ファイルの blob をツリーに追加します:
            :
            :
                  //. Tree 追加
                  var data4 = { tree: [] };
                  data4.tree = JSON.parse( JSON.stringify( body3.tree ) );
                  data4.tree.push( { path: originalfilename, mode: '100644', type: 'blob', sha: sha1 } );

                  var option4 = {
                    url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees',
                    headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                    json: data4,
                    method: 'POST'
                  };
                  request( option4, async function( err4, res4, body4 ){
                    if( err4 ){
                      console.log( { err4 } );
                      fs.unlink( filepath, function( e ){} );
                      resolve( false );
                    }else{
                      //body4 = JSON.parse( body4 );
                      console.log( { body4 } );
                      var sha4 = body4.sha;

            :
            :

そして(4)この状態をコミットします:
            :
            :
                      //. 現在の Commit の SHA を取得
                      var option5 = {
                        url: 'https://api.github.com/repos/' + settings.repo_name + '/branches/' + id,
                        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                        method: 'GET'
                      };
                      request( option5, async function( err5, res5, body5 ){
                        if( err5 ){
                          console.log( { err5 } );
                          fs.unlink( filepath, function( e ){} );
                          resolve( false );
                        }else{
                          body5 = JSON.parse( body5 );
                          console.log( { body5 } );
                          var sha5 = body5.commit.sha;

                          //. Commit を作成
                          var ts = ( new Date() ).getTime();
                          var data6 = {
                            message: '' + ts,
                            tree: sha4,
                            parents: [ sha5 ]
                          };
                          var option6 = {
                            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/commits',
                            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                            json: data6,
                            method: 'POST'
                          };
                          request( option6, async function( err6, res6, body6 ){
                            if( err6 ){
                              console.log( { err6 } );
                              fs.unlink( filepath, function( e ){} );
                              resolve( false );
                            }else{
                              //body6 = JSON.parse( body6 );
                              console.log( { body6 } );
                              var sha6 = body6.sha;

            :
            :

最後にこの後の処理に備えて(5)リファレンス SHA を更新することで一連のアップロード処理は完了します:
            :
            :
                              //. リファレンスを更新
                              var data7 = {
                                force: false,
                                sha: sha6
                              };
                              var option7 = {
                                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/' + id,
                                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                                json: data7,
                                method: 'PATCH'
                              };
                              request( option7, async function( err7, res7, body7 ){
                                if( err7 ){
                                  console.log( { err7 } );
                                  fs.unlink( filepath, function( e ){} );
                                  resolve( false );
                                }else{
                                  //body7 = JSON.parse( body7 );
                                  console.log( { body7 } );
                                  fs.unlink( filepath, function( e ){} );

                                  var sha7 = body7.object.sha;

                                  resolve( { sha: sha7 } );
                                }
                              });

            :
            :

これ以外にもブランチからファイルを削除するなど、他に実装している内容もありますが、なんとなく API を実行する際の共通の流れ(変更する場合は直前の状態の SHA を取得し、その値を指定して変更の POST を実行)がわかるのではないかと思います。

また上のソースコードの最後の Github API 実行部分をよく見ると、新しい状態の SHA を取得するための REST API のメソッドが珍しい PATCH になっていることがわかります。メソッドの種類として存在は知っていましたが、実際に使ったのは初めてのような気がします。それくらい珍しい REST API でした。