From 2ceb6e8c986f7e77f0d74aaac065fc969d39fc3e Mon Sep 17 00:00:00 2001 From: HampusM Date: Mon, 21 Jun 2021 14:12:05 +0200 Subject: Added branch & tag API endpoints, tag tarball downloading and added handlers to the api --- packages/server/package.json | 2 + packages/server/src/api/git.ts | 126 ++++++++++++++++++++++++++++++++++++++++- packages/server/src/api/v1.ts | 44 ++++++++++++++ packages/server/src/app.ts | 9 +++ 4 files changed, 178 insertions(+), 3 deletions(-) (limited to 'packages') diff --git a/packages/server/package.json b/packages/server/package.json index 90e54ea..1a9f352 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -14,12 +14,14 @@ "js-yaml": "^4.1.0", "nodegit": "^0.27.0", "nodemon": "^2.0.7", + "tar-stream": "^2.2.0", "whatwg-url": "^8.5.0" }, "devDependencies": { "@types/js-yaml": "^4.0.1", "@types/node": "^15.12.1", "@types/nodegit": "^0.27.2", + "@types/tar-stream": "^2.2.0", "@types/whatwg-url": "^8.2.0", "@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/parser": "^4.26.0", diff --git a/packages/server/src/api/git.ts b/packages/server/src/api/git.ts index 880e5c0..97b0d88 100644 --- a/packages/server/src/api/git.ts +++ b/packages/server/src/api/git.ts @@ -1,4 +1,4 @@ -import { ConvenientHunk, ConvenientPatch, Repository, Revwalk, TreeEntry } from "nodegit"; +import { Commit, ConvenientHunk, ConvenientPatch, Object, Repository, Revwalk, Tag, TreeEntry } from "nodegit"; import { FastifyReply, FastifyRequest } from "fastify"; import { join, parse } from "path"; import { readFile, readdir } from "fs"; @@ -6,6 +6,9 @@ import { IncomingMessage } from "http"; import { URL } from "whatwg-url"; import { spawn } from "child_process"; import { verifyGitRequest } from "./util"; +import { pack } from "tar-stream"; +import { pipeline } from "stream"; +import { createGzip } from "zlib"; export declare namespace Git { interface Hunk { @@ -73,7 +76,7 @@ export declare namespace Git { } // eslint-disable-next-line no-unused-vars - type Commit = { + type ShortCommit = { id: string, author: string, message: string, @@ -321,7 +324,7 @@ export class GitAPI { }, Promise.resolve([])); } - async getCommit(repo_name: string, commit_oid: string): Promise { + async getCommit(repo_name: string, commit_oid: string): Promise { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); const commit = await repo.getCommit(commit_oid); @@ -484,4 +487,121 @@ export class GitAPI { return Boolean(readme); } + + async getBranches(repo_name: string) { + const full_repo_name = addRepoDirSuffix(repo_name); + const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); + + const references = await repo.getReferences(); + + return references.filter(ref => ref.isBranch()).map(ref => { + return { + id: ref.target().tostrS(), + name: ref.shorthand() + } + }); + } + + async getBranch(repo_name: string, branch_id: string) { + const full_repo_name = addRepoDirSuffix(repo_name); + const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); + + const references = await repo.getReferences(); + const branches = references.filter(ref => ref.isBranch()); + + const branch = branches.find(_branch => _branch.target().tostrS() === branch_id); + if(!branch) { + return null; + } + + const latest_commit = await repo.getBranchCommit(branch); + + return { + name: branch.shorthand(), + latest_commit: { + id: latest_commit.sha(), + message: latest_commit.message(), + date: latest_commit.time() + } + }; + } + + async getTags(repo_name: string) { + const full_repo_name = addRepoDirSuffix(repo_name); + const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); + + const references = await repo.getReferences(); + + return Promise.all(references.filter(ref => ref.isTag()).map(async ref => { + const tagger = (await Tag.lookup(repo, ref.target())).tagger(); + + return { + name: ref.shorthand(), + author: { + name: tagger.name(), + email: tagger.email() + }, + date: tagger.when().time() + }; + })); + } + + async downloadTagArchive(repo_name: string, tag_name: string, reply: FastifyReply): Promise { + const full_repo_name = addRepoDirSuffix(repo_name); + const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); + + const reference = await repo.getReference(tag_name) + .catch(() => { + reply.code(404).send("Tag not found!"); + return null; + }); + if(!reference) { + return; + } + + let tree; + + try { + const commit = await Commit.lookup(repo, (await reference.peel(Object.TYPE.COMMIT)).id()) + tree = await commit.getTree() + } + catch { + reply.code(500).send("Internal server error!"); + return; + } + + const archive = pack(); + const gzip = createGzip(); + + reply.raw.writeHead(200, { + "Content-Encoding": "gzip", + "Content-Type": "application/gzip", + "Content-Disposition": `attachment; filename="${repo_name}-${tag_name}.tar.gz"` + }); + + pipeline(archive, gzip, reply.raw, () => reply.raw.end()); + + gzip.on("close", () => reply.raw.end()); + gzip.on("error", () => reply.raw.end()); + archive.on("error", () => reply.raw.end()); + + async function addArchiveEntries(entries: TreeEntry[]) { + for(const tree_entry of entries) { + if(tree_entry.isBlob()) { + const blob = await tree_entry.getBlob(); + archive.entry({ name: `${repo_name}/${tree_entry.path()}` }, blob.content().toString()); + } + else if(tree_entry.isTree()) { + await addArchiveEntries((await tree_entry.getTree()).entries()); + } + } + } + + addArchiveEntries(tree.entries()) + .then(() => archive.finalize()) + .catch(() => { + archive.finalize(); + reply.raw.end(); + }); + } }; \ No newline at end of file diff --git a/packages/server/src/api/v1.ts b/packages/server/src/api/v1.ts index e6391a0..b75c473 100644 --- a/packages/server/src/api/v1.ts +++ b/packages/server/src/api/v1.ts @@ -6,6 +6,13 @@ import { GitAPI } from "./git"; export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, done: any) { const git = new GitAPI(opts.config.settings.base_dir); + fastify.setErrorHandler((err, req, reply) => { + reply.code(500).send({ error: "Internal server error!" }); + }); + fastify.setNotFoundHandler((req, reply) => { + reply.code(404).send({ error: "Endpoint not found!" }); + }) + fastify.route({ method: "GET", url: "/info", @@ -107,6 +114,43 @@ export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, do } }); + fastify_repo.route({ + method: "GET", + url: "/branches", + handler: async(req, reply) => { + const params: any = req.params; + const branches = await git.getBranches(params.repo); + + reply.send({ data: branches }); + } + }); + + fastify_repo.route({ + method: "GET", + url: "/branches/:branch", + handler: async(req, reply) => { + const params: any = req.params; + const branch = await git.getBranch(params.repo, params.branch); + + if(!branch) { + reply.code(404).send({ error: "Branch not found!" }); + return; + } + + reply.send({ data: branch }); + } + }); + + fastify_repo.route({ + method: "GET", + url: "/tags", + handler: async(req, reply) => { + const params: any = req.params; + const refs = await git.getTags(params.repo); + reply.send({ data: refs }); + } + }); + done_repo(); }, { prefix: "/repos/:repo" }); diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index d5f63b0..263703b 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -117,6 +117,15 @@ fastify.route({ } }); +fastify.route({ + method: "GET", + url: "/:repo([a-zA-Z0-9\\.\\-_]+)/refs/tags/:tag", + handler: (req, reply) => { + const params: any = req.params; + git.downloadTagArchive(params.repo, params.tag, reply); + } +}); + fastify.listen(settings.port, settings.host, (err: Error, addr: string) => { if(err) { console.error(err); -- cgit v1.2.3-18-g5258