发布于 2026-01-06 0 阅读
0

如何使用 Octokit 和 TypeScript 以编程方式将文件推送到代码仓库

如何使用 Octokit 和 TypeScript 以编程方式将文件推送到代码仓库

我一直很喜欢“程序化地”这个词,而且时不时地,我在谷歌上搜索某个具体问题时也会用到它。通常情况下,以“程序化”的方式做某事能为一些实际问题找到优雅的解决方案,或者通过自动化任务节省时间。

上周我参与开发一款产品,其中一项挑战就是处理 GitHub代码库。我之前也用过 GitHub 的 API,但主要是用于 GitHub Actions 和一些持续集成方面的内容,而这次的情况略有不同,因为它的核心是 Git,而 Git 并不简单

基本上,我需要创建一个仓库(如果不存在),然后向其中推送n 个文件。一开始,我尝试直接使用GitHub API个人访问令牌作为身份验证方式,测试我猜测的实现方法。之后,我开始使用 GitHub 的Octokit/REST来编写解决方案。Octokit /REST是一个很棒的 AP​​I 封装库,支持 TypeScript,对你来说会很有帮助。

我必须提一下,这是我第一次真正感受到 TypeScript 对我工作效率的巨大提升。完成第二步后,我没有停下来进行测试,完成后,我几乎可以肯定自己会遗漏某些步骤,但解决方案第一次就完美运行了。

所以,我首先关心的问题之一就是,我该如何模拟git add . && git commit -m "My commit" && git push使用 API?经过一番谷歌搜索后,我在白板上写道:

注意:这里的大部分引用都使用了 SHA,即Git 中对象的哈希值。每个提交都有的、你看到的“代码”git log就是该提交的 SHA 值。

  1. 获取ref要推送文件的分支的 SHA 值。你需要的是最后一个提交 文档的 SHA 值。

  2. 通过提交的SHA 值,您可以获取该提交的文件 ,也就是实际存储文件的数据结构。您还需要它的 SHA值。

  3. 接下来,你需要一份要上传的文件列表,包括文件的路径和内容。有了这些信息,你可以为每个文件创建一个 blob。就我而言,我处理的是.mds 和.yamls,所以我使用了utf8编码功能,并将从 中获取的每个文件的内容作为纯文本发送fs.readFile(path, 'utf8')。但对于二进制文件、子模块、空目录和符号链接,还有其他处理方法。哦,对了:不用担心目录内的文件下一步只需发送每个文件的路径,GitHub 会自动进行相应的组织(这正是我之前担心需要手动处理的部分)。

  4. 利用步骤 3中创建的所有 blob 的 SHA 值,您将为仓库创建一个包含这些文件的新树。在这里,您需要将 blob 链接到路径,并将所有路径链接到树中。文档中介绍了一些模式100644,但我只使用了常规文件模式。您还需要将步骤 2 中获取的 SHA 设置为您正在创建的树的父树。

  5. 有了包含所有文件的树状结构(及其 SHA 值),你需要创建一个指向该树状结构的新提交。这个提交将包含你对仓库的所有更改。它还必须以步骤 1 中获取的提交 SHA 值作为其父提交。请注意,这是 Git 中的常见模式。你也可以在这里设置提交消息

  6. 最后,使用提交返回的 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,
  })
Enter fullscreen mode Exit fullscreen mode

https://gist.github.com/luciannojunior/864849a7f3c347be86862a3a43994fe0

如有任何疑问,欢迎随时联系我 :)

文章来源:https://dev.to/lucis/how-to-push-files-programatically-to-a-repository-using-octokit-with-typescript-1nj0