diff options
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/git.js | 415 | ||||
-rw-r--r-- | src/api/util.js | 45 | ||||
-rw-r--r-- | src/api/v1.js | 136 |
3 files changed, 0 insertions, 596 deletions
diff --git a/src/api/git.js b/src/api/git.js deleted file mode 100644 index 95759f0..0000000 --- a/src/api/git.js +++ /dev/null @@ -1,415 +0,0 @@ -const { formatDistance } = require('date-fns'); -const fs = require('fs'); -const git = require("nodegit"); -const zlib = require("zlib"); -const { spawn } = require('child_process'); -const whatwg = require("whatwg-url"); -const path = require("path"); - -function addRepoDirSuffix(repo_name) -{ - if(!repo_name.endsWith(".git")) { - return repo_name + ".git"; - } - return repo_name; -} - -async function getLog(base_dir, repo_name) -{ - const repo = await git.Repository.openBare(`${base_dir}/${repo_name}`); - - const walker = git.Revwalk.create(repo); - walker.pushHead(); - - const raw_commits = await walker.getCommitsUntil(() => true); - - const commits = Promise.all(raw_commits.map(async commit => ({ - commit: commit.sha(), - author_full: commit.author().toString(), - author_name: commit.author().name(), - author_email: commit.author().email(), - date: commit.date(), - message: commit.message().replace(/\n/g, ""), - insertions: (await (await commit.getDiff())[0].getStats()).insertions(), - deletions: (await (await commit.getDiff())[0].getStats()).deletions(), - files_changed: (await (await commit.getDiff())[0].getStats()).filesChanged() - }))); - - return await commits; -} - -async function getTimeSinceLatestCommit(base_dir, repo_name) -{ - const repo = await git.Repository.openBare(`${base_dir}/${repo_name}`); - const master_commit = await repo.getMasterCommit(); - - return formatDistance(new Date(), master_commit.date()); -} - -function getRepoFile(base_dir, repo, file) -{ - return new Promise(resolve => - { - fs.readFile(`${base_dir}/${repo}/${file}`, async (err, content) => - { - if(!err) { - resolve(content.toString().replace(/\n/g, "")); - return; - } - resolve(""); - }); - }); -} - -function getRepos(base_dir) -{ - return new Promise((resolve) => - { - fs.readdir(base_dir, (err, dir_content) => - { - if(err) { - resolve({ "error": err }); - return; - } - - dir_content.filter(repo => repo.endsWith(".git")).reduce((acc, repo) => - { - return acc.then((repos) => - { - return getRepoFile(base_dir, repo, "description").then((description) => - { - return getRepoFile(base_dir, repo, "owner").then((owner) => - { - return getTimeSinceLatestCommit(base_dir, repo).then((last_commit_date) => - { - repos[repo.slice(0, -4)] = { "description": description, "owner": owner, "last_updated": last_commit_date }; - return repos; - }); - }); - }); - }); - }, Promise.resolve({})).then((repos) => - { - resolve(repos); - }); - }); - }); -} - -function parseHunkAddDel(hunk) -{ - let new_lines = []; - let deleted_lines = []; - - hunk.forEach((line, index) => - { - if(line.charAt(0) === '+') { - hunk[index] = line.slice(1); - new_lines.push(index); - } - else if(line.charAt(0) === '-') { - hunk[index] = line.slice(1); - deleted_lines.push(index); - } - }); - - return { new: new_lines, deleted: deleted_lines, hunk: hunk.join("\n") }; -} - -async function getCommit(base_dir, repo_name, commit_oid) -{ - repo_name = addRepoDirSuffix(repo_name); - - const repo = await git.Repository.openBare(`${base_dir}/${repo_name}`); - const commit = await repo.getCommit(commit_oid); - const diff = (await commit.getDiff())[0]; - const all_patches = (await diff.toBuf(1)).split('\n'); - - // Get the count of lines for all of patches's headers - const patch_headers = (await diff.toBuf(2)).split('\n'); - const patch_header_data = await patch_headers.reduce((acc, line, index) => - { - return acc.then((arr) => - { - if(/^diff --git/.test(line)) { - arr[0].push(all_patches.indexOf(line)); - - if(arr[2] != undefined) { - arr[1].push(patch_headers.slice(arr[2], index).length); - } - arr[2] = index; - } - else if(index == patch_headers.length - 1 && arr[2] != undefined) { - arr[1].push(patch_headers.slice(arr[2], index).length); - } - return arr; - }); - }, Promise.resolve([ [], [], undefined ])); - - console.log(patch_header_data); - - const patches = await diff.patches(); - const parsed_patches = patches.reduce((acc, patch, patch_index) => - { - return acc.then((arr) => - { - return patch.hunks().then((hunks) => - { - console.log("\n" + patch.newFile().path()); - - const patch_start = patch_header_data[0][patch_index] + patch_header_data[1][patch_index]; - const patch_end = (patch_header_data[0][patch_index + 1] !== undefined) ? patch_header_data[0][patch_index + 1] : all_patches.length - 1; - const patch_content = all_patches.slice(patch_start, patch_end); - - const line_lengths = patch_content.map((line) => line.length).reduce((acc, length) => acc + length); - - if(patch_content.length > 5000 || line_lengths > 5000) { - console.log("Too large!"); - - arr.push({ - from: patch.oldFile().path(), - to: patch.newFile().path(), - additions: patch.lineStats()["total_additions"], - deletions: patch.lineStats()["total_deletions"], - too_large: true, - hunks: null - }); - return arr; - } - - // Go through all of the patch's hunks - // Patches are split into parts of where in the file the change is made. Those parts are called hunks. - return hunks.reduce((acc, hunk, hunk_index) => - { - return acc.then((hunks_data) => - { - const hunk_header = hunk.header(); - const hunk_header_index = patch_content.indexOf(hunk_header.replace(/\n/g, "")); - - if(hunks_data[0] !== undefined) { - const prev_hunk = hunks[hunk_index - 1]; - hunks_data[1].push(Object.assign({ - new_start: prev_hunk.newStart(), - new_lines: prev_hunk.newLines(), - old_start: prev_hunk.oldStart(), - old_lines: prev_hunk.oldLines() - }, parseHunkAddDel(patch_content.slice(hunks_data[0], hunk_header_index)))); - - hunks_data[2] = hunks_data + patch_content.slice(hunks_data[0], hunk_header_index).length; - } - - hunks_data[0] = hunk_header_index; - return hunks_data; - }); - }, Promise.resolve([ undefined, [], 0 ])).then((hunks_data) => - { - const prev_hunk = hunks[hunks.length - 1]; - hunks_data[1].push(Object.assign({ - new_start: prev_hunk.newStart(), - new_lines: prev_hunk.newLines(), - old_start: prev_hunk.oldStart(), - old_lines: prev_hunk.oldLines() - }, parseHunkAddDel(patch_content.slice(hunks_data[0], patch_end)))); - - arr.push({ - from: patch.oldFile().path(), - to: patch.isDeleted() ? "/dev/null" : patch.newFile().path(), - additions: patch.lineStats()["total_additions"], - deletions: patch.lineStats()["total_deletions"], - too_large: false, - hunks: hunks_data[1] - }); - - return arr; - }); - }); - }); - }, Promise.resolve([])); - - return { - hash: commit.sha(), - author: commit.author().toString(), - message: commit.message(), - date: commit.date(), - patches: await parsed_patches - }; -} - -async function doesCommitExist(base_dir, repo_name, commit_oid) -{ - const repo = await git.Repository.openBare(`${base_dir}/${repo_name}`); - - try { - await repo.getCommit(commit_oid); - return true; - } - catch { - return false; - } -} - -function connectToGitHTTPBackend(base_dir, req, reply) -{ - const url_path = req.url.replace(req.params.repo, req.params.repo + ".git"); - const repo = req.params.repo + ".git"; - const repo_path = path.join(base_dir, repo); - - req = req.headers['Content-Encoding'] == 'gzip' ? req.pipe(zlib.createGunzip()) : req; - - const parsed_url = new whatwg.URL(`${req.protocol}://${req.hostname}${url_path}`); - const url_path_parts = parsed_url.pathname.split('/'); - - let service; - let info = false; - - if(/\/info\/refs$/.test(parsed_url.pathname)) { - service = parsed_url.searchParams.get("service"); - info = true; - } - else { - service = url_path_parts[url_path_parts.length-1]; - } - - const content_type = `application/x-${service}-${info ? "advertisement" : "result"}`; - - if(/\.\/|\.\./.test(parsed_url.pathname)) { - reply.header("Content-Type", content_type); - reply.code(404).send("Git repository not found!\n"); - return; - } - - if(service !== 'git-upload-pack') { - reply.header("Content-Type", content_type); - reply.code(403).send("Access denied!\n"); - return; - } - - reply.raw.writeHead(200, { "Content-Type": content_type }); - - const spawn_args = [ "--stateless-rpc", repo_path ]; - - if(info) { - spawn_args.push("--advertise-refs"); - } - - const git_pack = spawn(service, spawn_args); - - if(info) { - const s = '# service=' + service + '\n'; - const n = (4 + s.length).toString(16); - reply.raw.write(Buffer.from((Array(4 - n.length + 1).join('0') + n + s) + '0000')); - } - else { - req.body.on("data", (data) => git_pack.stdin.write(data)); - req.body.on("close", () => git_pack.stdin.end()); - } - - git_pack.on("error", (err) => console.log(err)); - git_pack.stderr.on("data", (stderr) => console.log(stderr)); - - git_pack.stdout.on("data", (data) => - { - reply.raw.write(data); - }); - - git_pack.on("close", () => reply.raw.end()); -} - -async function getTree(base_dir, repo_name, tree_path) -{ - repo_name = addRepoDirSuffix(repo_name); - - const repo = await git.Repository.openBare(`${base_dir}/${repo_name}`); - const master_commit = await repo.getMasterCommit(); - - const tree = await master_commit.getTree(); - - let entries; - if(tree_path) { - try { - const path_entry = await tree.getEntry(tree_path); - - if(path_entry.isBlob()) { - return { type: "blob", content: (await path_entry.getBlob()).content().toString() }; - } - - entries = await (await path_entry.getTree()).entries(); - } - catch(err) { - if(err.errno === -3) { - return { error: 404 }; - } - return { error: 500 }; - } - } - else { - entries = tree.entries(); - } - - return { type: "tree", tree: await entries.reduce((acc, entry) => - { - return acc.then((obj) => - { - return getTreeEntryLastCommit(repo, entry).then((last_commit) => - { - obj[path.parse(entry.path()).base] = { - oid: entry.oid(), - type: entry.isBlob() ? "blob" : "tree", - last_commit: { - id: last_commit.id, - message: last_commit.message, - time: last_commit.time - } - }; - return obj; - }); - }); - }, Promise.resolve({})) }; -} - -async function getTreeEntryLastCommit(repo, tree_entry) -{ - const walker = git.Revwalk.create(repo); - walker.pushHead(); - - const raw_commits = await walker.getCommitsUntil(() => true); - - return raw_commits.reduce((acc, commit) => - { - return acc.then((obj) => - { - if(Object.keys(obj).length == 0) { - return commit.getDiff().then((diffs) => - { - return diffs[0].patches().then((patches) => - { - let matching_path_patch; - if(tree_entry.isBlob()) { - matching_path_patch = patches.find((patch) => patch.newFile().path() === tree_entry.path()); - } - else { - matching_path_patch = patches.find((patch) => path.parse(patch.newFile().path()).dir.startsWith(tree_entry.path())); - } - - if(matching_path_patch) { - obj.id = commit.sha(); - obj.message = commit.message().replace(/\n/g, ""); - obj.time = commit.date(); - } - return obj; - }); - }); - } - - return obj; - }); - }, Promise.resolve({})); -} - -module.exports.getLog = getLog; -module.exports.getRepos = getRepos; -module.exports.getRepoFile = getRepoFile; -module.exports.getCommit = getCommit; -module.exports.doesCommitExist = doesCommitExist; -module.exports.connectToGitHTTPBackend = connectToGitHTTPBackend; -module.exports.getTree = getTree;
\ No newline at end of file diff --git a/src/api/util.js b/src/api/util.js deleted file mode 100644 index aa31296..0000000 --- a/src/api/util.js +++ /dev/null @@ -1,45 +0,0 @@ -const fs = require("fs"); -const git = require("./git"); - -function verifyRepoName(dirty, base_dir) -{ - return new Promise((resolve) => - { - const is_valid_repo_name = /^[a-zA-Z0-9\\.\-_]+$/.test(dirty); - if(!is_valid_repo_name) { - resolve("ERR_REPO_REGEX"); - } - - fs.readdir(base_dir, (err, dir_content) => - { - if(err) { - resolve("ERR_REPO_NOT_FOUND"); - } - - dir_content = dir_content.filter(repo => repo.endsWith(".git")); - if(!dir_content.includes(dirty + ".git")) { - resolve("ERR_REPO_NOT_FOUND"); - } - - resolve(true); - }); - }); -} - -async function verifyCommitID(base_dir, repo, dirty) -{ - if(!/^[a-fA-F0-9]+$/.test(dirty)) { - return "ERR_COMMIT_REGEX"; - } - - const commit_exists = await git.doesCommitExist(base_dir, repo, dirty); - - if(!commit_exists) { - return "ERR_COMMIT_NOT_FOUND"; - } - - return true; -} - -module.exports.verifyRepoName = verifyRepoName; -module.exports.verifyCommitID = verifyCommitID;
\ No newline at end of file diff --git a/src/api/v1.js b/src/api/v1.js deleted file mode 100644 index 25a8019..0000000 --- a/src/api/v1.js +++ /dev/null @@ -1,136 +0,0 @@ -const git = require("./git"); -const util = require("./util"); - -module.exports = function (fastify, opts, done) -{ - fastify.route({ - method: "GET", - path: "/info", - handler: (req, reply) => - { - reply.send({ data: opts.config.settings }); - } - }); - fastify.route({ - method: "GET", - path: "/repos", - handler: async (req, reply) => - { - let repos = await git.getRepos(opts.config.settings.base_dir); - - if(repos["error"]) { - reply.code(500).send({ error: "Internal server error!" }); - return; - } - - reply.send({ data: repos }); - } - }); - - fastify.route({ - method: "GET", - path: "/repos/:repo", - handler: async (req, reply) => - { - const repo_verification = await util.verifyRepoName(req.params.repo, opts.config.settings.base_dir); - if(repo_verification !== true) { - if(repo_verification === "ERR_REPO_REGEX") { - reply.code(400).send({ error: "Unacceptable git repository name!" }); - } - else if(repo_verification === "ERR_REPO_NOT_FOUND") { - reply.code(404).send({ error: "Git repository not found!" }); - } - } - - const repo = `${req.params.repo}.git`; - const desc = await git.getRepoFile(opts.config.settings.base_dir, repo, "description"); - - reply.send({ data: { name: req.params.repo, description: desc } }); - } - }); - - fastify.register((fastify_repo, opts_repo, done_repo) => - { - fastify_repo.addHook("onRequest", async (req, reply) => - { - const repo_verification = await util.verifyRepoName(req.params.repo, opts.config.settings.base_dir); - if(repo_verification !== true) { - if(repo_verification === "ERR_REPO_REGEX") { - reply.code(400).send({ error: "Unacceptable git repository name!" }); - } - else if(repo_verification === "ERR_REPO_NOT_FOUND") { - reply.code(404).send({ error: "Git repository not found!" }); - } - } - }); - - fastify_repo.route({ - method: "GET", - path: "/log", - handler: async (req, reply) => - { - const log = await git.getLog(opts.config.settings.base_dir, req.params.repo + ".git"); - - if(log["error"]) { - if(typeof log["error"] === "string") { - reply.code(500).send({ error: log["error"] }); - } - - switch(log["error"]) { - case 404: - reply.code(404).send({ error: "Git repository not found!" }); - } - - return; - } - reply.send({ data: log }); - } - }); - - fastify_repo.route({ - method: "GET", - path: "/log/:commit", - handler: async (req, reply) => - { - const commit_verification = await util.verifyCommitID(opts.config.settings.base_dir, req.params.repo + ".git", req.params.commit); - if(!commit_verification !== true) { - if(commit_verification === "ERR_COMMIT_REGEX") { - reply.code(400).send({ error: "Unacceptable commit id!" }); - } - else if(commit_verification === "ERR_COMMIT_NOT_FOUND") { - reply.code(404).send({ error: "Commit not found!" }); - } - } - - const commit = await git.getCommit(opts.config.settings.base_dir, req.params.repo, req.params.commit); - - reply.send({ data: commit }); - } - }); - - fastify_repo.route({ - method: "GET", - path: "/tree", - handler: async (req, reply) => - { - const tree_path = (req.query.length !== 0 && req.query.path) ? req.query.path : null; - - const tree = await git.getTree(opts.config.settings.base_dir, req.params.repo, tree_path); - - if(tree.error) { - if(tree.error === 404) { - reply.code(404).send({ error: "Path not found" }); - } - else { - reply.code(500).send({ error: "Internal server error" }); - } - } - reply.send({ data: tree }); - } - }); - - done_repo(); - }, { prefix: "/repos/:repo" }); - - done(); -};
\ No newline at end of file |