aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/server/package.json2
-rw-r--r--packages/server/src/api/git.ts126
-rw-r--r--packages/server/src/api/v1.ts44
-rw-r--r--packages/server/src/app.ts9
-rw-r--r--yarn.lock41
5 files changed, 216 insertions, 6 deletions
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(<Git.ShortRepository[]>[]));
}
- async getCommit(repo_name: string, commit_oid: string): Promise<Git.Commit> {
+ async getCommit(repo_name: string, commit_oid: string): Promise<Git.ShortCommit> {
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<void> {
+ 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);
diff --git a/yarn.lock b/yarn.lock
index aff8196..564756e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1230,6 +1230,13 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.7.tgz#545158342f949e8fd3bfd813224971ecddc3fac4"
integrity sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ==
+"@types/tar-stream@^2.2.0":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@types/tar-stream/-/tar-stream-2.2.0.tgz#2778ef8e328a520959a39681c15c83c53553426f"
+ integrity sha512-sRTpT180sVigzD4SiCWJQQrqcdkWnmscWvx+cXvAoPtXbLFC5+QmKi2xwRcPe4iRu0GcVl1qTeJKUTS5hULfrw==
+ dependencies:
+ "@types/node" "*"
+
"@types/uglify-js@*":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124"
@@ -2302,7 +2309,7 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-base64-js@^1.0.2:
+base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@@ -2377,6 +2384,15 @@ bl@^1.0.0:
readable-stream "^2.3.5"
safe-buffer "^5.1.1"
+bl@^4.0.3:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
+ integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
+ dependencies:
+ buffer "^5.5.0"
+ inherits "^2.0.4"
+ readable-stream "^3.4.0"
+
bluebird@^3.1.1, bluebird@^3.5.5, bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
@@ -2594,6 +2610,14 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
+buffer@^5.5.0:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+ integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.1.13"
+
builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@@ -3926,7 +3950,7 @@ encoding-negotiator@^2.0.1:
resolved "https://registry.yarnpkg.com/encoding-negotiator/-/encoding-negotiator-2.0.1.tgz#79871bb5473b81f6a0670e8de5303fb5ee0868a3"
integrity sha512-GSK7qphNR4iPcejfAlZxKDoz3xMhnspwImK+Af5WhePS9jUpK/Oh7rUdyENWu+9rgDflOCTmAojBsgsvM8neAQ==
-end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
@@ -5447,7 +5471,7 @@ icss-utils@^5.0.0:
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
-ieee754@^1.1.4:
+ieee754@^1.1.13, ieee754@^1.1.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -9412,6 +9436,17 @@ tar-stream@^1.1.2:
to-buffer "^1.1.1"
xtend "^4.0.0"
+tar-stream@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
+ integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
+ dependencies:
+ bl "^4.0.3"
+ end-of-stream "^1.4.1"
+ fs-constants "^1.0.0"
+ inherits "^2.0.3"
+ readable-stream "^3.1.1"
+
tar@^4, tar@^4.4.8:
version "4.4.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"