import { Octokit } from "@octokit/rest" ; import { KoreFileAdaptor } from "./KoreFileAdaptor" ; import { encode as arrayBufferToBase64 } from "base64-arraybuffer" ; import type { components } from "@octokit/openapi-types" ; import { fromBase64 } from "js-base64" ; type GetRepoContentResponseDataFile = components [ "schemas" ] [ "content-file" ] ; const debug = require ( "debug" ) ( "korefile" ) ; const GITHUB_API_TOKEN = typeof process === "object" && process . env . GITHUB_API_TOKEN ; export interface GitCommitPushOptions { owner : string ; repo : string ; files : { path : string ; content : string | ArrayBuffer ; } [ ] ; ref : string ; forceUpdate ? : boolean ; commitMessage ? : string ; token ? : string ; } const getReferenceCommit = function ( octokit , options ) { return octokit . git . getRef ( { owner : options . owner , repo : options . repo , ref : options . ref } ) . then ( ( res ) => { debug ( "getReferenceCommit Response: %O" , res ) ; return { referenceCommitSha : res . data . object . sha } ; } ) ; } ; const createTree = function ( octokit , options , data ) { const promises = options . files . map ( ( file ) => { if ( typeof file . content === "string" ) { return octokit . git . createBlob ( { owner : options . owner , repo : options . repo , content : file . content , encoding : "utf-8" } ) . then ( ( blob ) => { debug ( "createBlob:text" ) ; return { sha : blob . data . sha , path : file . path , mode : "100644" , type : "blob" } as const ; } ) ; } else if ( ArrayBuffer . isView ( file . content ) ) { return octokit . git . createBlob ( { owner : options . owner , repo : options . repo , content : arrayBufferToBase64 ( file . content ) , encoding : "base64" } ) . then ( ( blob ) => { debug ( "createBlob:buffer" ) ; return { sha : blob . data . sha , path : file . path , mode : "100644" , type : "blob" } as const ; } ) ; } throw new Error ( `This file can not handled: ${ file }` ) ; } ) ; return Promise . all ( promises ) . then ( ( files ) => { debug ( "files: %O" , files ) ; return octokit . git . createTree ( { owner : options . owner , repo : options . repo , tree : files , base_tree : data . referenceCommitSha } ) . then ( ( res ) => { debug ( "createCommit Response: %O" , res ) ; return { ... data , newTreeSha : res . data . sha } ; } ) ; } ) ; } ; const createCommit = function ( octokit , options , data ) { return octokit . git . createCommit ( { owner : options . owner , repo : options . repo , message : options . commitMessage || "commit" , tree : data . newTreeSha , parents : [ data . referenceCommitSha ] } ) . then ( ( res ) => { debug ( "createTree Response: %O" , res ) ; return { ... data , newCommitSha : res . data . sha } ; } ) ; } ; const updateReference = function ( github , options , data ) { return github . git . updateRef ( { owner : options . owner , repo : options . repo , ref : options . ref , sha : data . newCommitSha , force : options . forceUpdate } ) . then ( ( res ) => { debug ( "updateReference Response: %O" , res ) ; } ) ; } ; export const getContent = ( github , { owner , repo , path , ref } ) => { return github . repos . getContent ( { owner , repo , path , ref } ) . then ( ( res ) => { const data = res . data as GetRepoContentResponseDataFile ; if ( Array . isArray ( data ) ) { throw new Error ( `folder does not support` ) ; } debug ( "getContent Response: %O" , res ) ; if ( data . type !== "file" ) { return Promise . reject ( new Error ( "This is not file:" + path ) ) ; } if ( data . encoding === "base64" ) { return Promise . resolve ( fromBase64 ( data . content ) ) ; } throw new Error ( "Unknown file type" + data . type + ":" + data . encoding ) ; } ) ; } ; export const deleteFile = async ( octokit , { owner , repo , path , ref , commitMessage } ) => { const response = await octokit . repos . getContent ( { owner , repo , path , ref } ) ; const { data } = response ; debug ( "deleteFile:getContents Response: %O" , data ) ; if ( Array . isArray ( data ) ) { throw new Error ( `folder does not support` ) ; } return octokit . repos . deleteFile ( { owner , repo , path , branch : ref . replace ( /^refs\// , "" ) . replace ( /^heads\// , "" ) , sha : data . sha , message : commitMessage } ) . then ( ( res ) => { debug ( "deleteFile:deleteFile Response: %O" , res ) ; } ) ; } ; export interface GitHubAdaptorOptions { host ? : string ; owner : string ; repo : string ; ref : string ; commitMessage ? : { write ? : ( filePath : string ) => string ; delete ? : ( filePath : string ) => string ; } ; forceUpdate ? : boolean ; token ? : string ; } export const createGitHubAdaptor = ( options ) : KoreFileAdaptor => { const token = options . token || GITHUB_API_TOKEN ; if ( ! token ) { throw new Error ( `token is not defined` ) ; } const octKit = new Octokit ( { auth : token , type : "oauth" , userAgent : "korefile" } ) ; const filledOptions = { owner : options . owner , repo : options . repo , ref : options . ref , forceUpdate : options . forceUpdate || false } ; return { readFile ( filePath ) { return getContent ( octKit , { repo : filledOptions . repo , owner : filledOptions . owner , path : filePath , ref : filledOptions . ref } ) ; } , writeFile ( filePath , content ) { const withFileOption = { ... filledOptions , files : [ { path : filePath , content : content } ] } ; const commitMessage = options . commitMessage && typeof options . commitMessage . write === "function" ? options . commitMessage . write ( filePath ) : `Update ${ filePath }` ; return getReferenceCommit ( octKit , filledOptions ) . then ( ( data ) => createTree ( octKit , withFileOption , data ) ) . then ( ( data ) => createCommit ( octKit , { ... filledOptions , commitMessage } , data ) ) . then ( ( data ) => updateReference ( octKit , withFileOption , data ) ) ; } , writeFiles ( files ) { const withFileOption = { ... filledOptions , files : files } ; const commitMessage = `Update files` ; return getReferenceCommit ( octKit , filledOptions ) . then ( ( data ) => createTree ( octKit , withFileOption , data ) ) . then ( ( data ) => createCommit ( octKit , { ... filledOptions , commitMessage } , data ) ) . then ( ( data ) => updateReference ( octKit , withFileOption , data ) ) ; } , deleteFile ( filePath ) { const commitMessage = options . commitMessage && typeof options . commitMessage . delete === "function" ? options . commitMessage . delete ( filePath ) : `Delete ${ filePath }` ; return deleteFile ( octKit , { ... filledOptions , path : filePath , commitMessage : commitMessage } ) ; } } ; } ;