如何使用 Octokit 和 TypeScript 以编程方式将文件推送到代码仓库
我一直很喜欢“程序化地”这个词,而且时不时地,我在谷歌上搜索某个具体问题时也会用到它。通常情况下,以“程序化”的方式做某事能为一些实际问题找到优雅的解决方案,或者通过自动化任务节省时间。
上周我参与开发一款产品,其中一项挑战就是处理 GitHub代码库。我之前也用过 GitHub 的 API,但主要是用于 GitHub Actions 和一些持续集成方面的内容,而这次的情况略有不同,因为它的核心是 Git,而 Git 并不简单。
基本上,我需要创建一个仓库(如果不存在),然后向其中推送n 个文件。一开始,我尝试直接使用GitHub API和个人访问令牌作为身份验证方式,测试我猜测的实现方法。之后,我开始使用 GitHub 的Octokit/REST来编写解决方案。Octokit /REST是一个很棒的 API 封装库,支持 TypeScript,对你来说会很有帮助。
我必须提一下,这是我第一次真正感受到 TypeScript 对我工作效率的巨大提升。完成第二步后,我没有停下来进行测试,完成后,我几乎可以肯定自己会遗漏某些步骤,但解决方案第一次就完美运行了。
所以,我首先关心的问题之一就是,我该如何模拟git add . && git commit -m "My commit" && git push使用 API?经过一番谷歌搜索后,我在白板上写道:
注意:这里的大部分引用都使用了 SHA,即Git 中对象的哈希值。每个提交都有的、你看到的“代码”git log就是该提交的 SHA 值。
-
获取
ref要推送文件的分支的 SHA 值。你需要的是最后一个提交 文档的 SHA 值。 -
通过提交的SHA 值,您可以获取该提交的文件 树,也就是实际存储文件的数据结构。您还需要它的 SHA值。
-
接下来,你需要一份要上传的文件列表,包括文件的路径和内容。有了这些信息,你可以为每个文件创建一个 blob。就我而言,我处理的是
.mds 和.yamls,所以我使用了utf8编码功能,并将从 中获取的每个文件的内容作为纯文本发送fs.readFile(path, 'utf8')。但对于二进制文件、子模块、空目录和符号链接,还有其他处理方法。哦,对了:不用担心目录内的文件,下一步只需发送每个文件的路径,GitHub 会自动进行相应的组织(这正是我之前担心需要手动处理的部分)。 -
利用步骤 3中创建的所有 blob 的 SHA 值,您将为仓库创建一个包含这些文件的新树。在这里,您需要将 blob 链接到路径,并将所有路径链接到树中。文档中介绍了一些模式
100644,但我只使用了常规文件模式。您还需要将步骤 2 中获取的 SHA 设置为您正在创建的树的父树。 -
有了包含所有文件的树状结构(及其 SHA 值),你需要创建一个指向该树状结构的新提交。这个提交将包含你对仓库的所有更改。它还必须以步骤 1 中获取的提交 SHA 值作为其父提交。请注意,这是 Git 中的常见模式。你也可以在这里设置提交消息。
-
最后,使用提交返回的 SHA文档,将分支引用设置为指向上一步创建的提交。
完毕!
我遇到的另一个问题是,有时我必须自己创建仓库,而没有现成的“初始”提交可供我使用。后来我发现,创建仓库时可以添加一个auto_init 参数,GitHub 会自动生成一个简单的 README.md 文件来初始化仓库。真是侥幸!
那么,让我们来看代码吧。
import Octokit from '@octokit/rest'
import glob from 'globby'
import path from 'path'
import { readFile } from 'fs-extra'
const main = async () => {
// There are other ways to authenticate, check https://developer.github.com/v3/#authentication
const octo = new Octokit({
auth: process.env.PERSONAL_ACESSS_TOKEN,
})
// For this, I was working on a organization repos, but it works for common repos also (replace org for owner)
const ORGANIZATION = `my-organization`
const REPO = `my-repo`
const repos = await octo.repos.listForOrg({
org: ORGANIZATION,
})
if (!repos.data.map((repo: Octokit.ReposListForOrgResponseItem) => repo.name).includes(REPO)) {
await createRepo(octo, ORGANIZATION, REPO)
}
/**
* my-local-folder has files on its root, and subdirectories with files
*/
await uploadToRepo(octo, `./my-local-folder`, ORGANIZATION, REPO)
}
main()
const createRepo = async (octo: Octokit, org: string, name: string) => {
await octo.repos.createInOrg({ org, name, auto_init: true })
}
const uploadToRepo = async (
octo: Octokit,
coursePath: string,
org: string,
repo: string,
branch: string = `master`
) => {
// gets commit's AND its tree's SHA
const currentCommit = await getCurrentCommit(octo, org, repo, branch)
const filesPaths = await glob(coursePath)
const filesBlobs = await Promise.all(filesPaths.map(createBlobForFile(octo, org, repo)))
const pathsForBlobs = filesPaths.map(fullPath => path.relative(coursePath, fullPath))
const newTree = await createNewTree(
octo,
org,
repo,
filesBlobs,
pathsForBlobs,
currentCommit.treeSha
)
const commitMessage = `My commit message`
const newCommit = await createNewCommit(
octo,
org,
repo,
commitMessage,
newTree.sha,
currentCommit.commitSha
)
await setBranchToCommit(octo, org, repo, branch, newCommit.sha)
}
const getCurrentCommit = async (
octo: Octokit,
org: string,
repo: string,
branch: string = 'master'
) => {
const { data: refData } = await octo.git.getRef({
owner: org,
repo,
ref: `heads/${branch}`,
})
const commitSha = refData.object.sha
const { data: commitData } = await octo.git.getCommit({
owner: org,
repo,
commit_sha: commitSha,
})
return {
commitSha,
treeSha: commitData.tree.sha,
}
}
// Notice that readFile's utf8 is typed differently from Github's utf-8
const getFileAsUTF8 = (filePath: string) => readFile(filePath, 'utf8')
const createBlobForFile = (octo: Octokit, org: string, repo: string) => async (
filePath: string
) => {
const content = await getFileAsUTF8(filePath)
const blobData = await octo.git.createBlob({
owner: org,
repo,
content,
encoding: 'utf-8',
})
return blobData.data
}
const createNewTree = async (
octo: Octokit,
owner: string,
repo: string,
blobs: Octokit.GitCreateBlobResponse[],
paths: string[],
parentTreeSha: string
) => {
// My custom config. Could be taken as parameters
const tree = blobs.map(({ sha }, index) => ({
path: paths[index],
mode: `100644`,
type: `blob`,
sha,
})) as Octokit.GitCreateTreeParamsTree[]
const { data } = await octo.git.createTree({
owner,
repo,
tree,
base_tree: parentTreeSha,
})
return data
}
const createNewCommit = async (
octo: Octokit,
org: string,
repo: string,
message: string,
currentTreeSha: string,
currentCommitSha: string
) =>
(await octo.git.createCommit({
owner: org,
repo,
message,
tree: currentTreeSha,
parents: [currentCommitSha],
})).data
const setBranchToCommit = (
octo: Octokit,
org: string,
repo: string,
branch: string = `master`,
commitSha: string
) =>
octo.git.updateRef({
owner: org,
repo,
ref: `heads/${branch}`,
sha: commitSha,
})
https://gist.github.com/luciannojunior/864849a7f3c347be86862a3a43994fe0
如有任何疑问,欢迎随时联系我 :)
文章来源:https://dev.to/lucis/how-to-push-files-programatically-to-a-repository-using-octokit-with-typescript-1nj0