diff options
-rw-r--r-- | api/git.js | 379 | ||||
-rw-r--r-- | api/v1.js | 10 | ||||
-rw-r--r-- | app.js | 34 |
3 files changed, 177 insertions, 246 deletions
@@ -1,75 +1,50 @@ -const { spawn } = require("child_process"); const { formatDistance } = require('date-fns'); const fs = require('fs'); +const git = require("nodegit"); -function execGit(path, action, args = [""], error) +function addRepoDirSuffix(repo_name) { - const git = spawn("git", ["-C", path, action, args].concat(args)); - - git.on("error", (err) => - { - const no_such_file_or_dir = new RegExp(`cannot change to '${path.replace('/', "\/")}': No such file or directory\\n$`); - - if(no_such_file_or_dir.test(err.toString())) { - error({ "error": 404 }) - return; - } - error({ "error": err }); - }); - - git.stderr.on("data", (err) => error({ "error": "Failed to communicate with git!", "message": err.toString() })); - - return git; + if(!repo_name.endsWith(".git")) { + return repo_name + ".git"; + } + return repo_name; } -function getLog(base_dir, path) +function getLog(base_dir, repo_name) { - return new Promise((resolve) => + return new Promise(async (resolve) => { - let log = []; + const repo = await git.Repository.openBare(`${base_dir}/${repo_name}`) - const log_format='{"hash":"%H","author":"%an","author_email":"%ae","date":"%at","subject":"%s"}'; - const git = execGit(`${base_dir}/${path}`, "log", [`--format=format:${log_format}`, "--shortstat"], (err) => resolve(err)); - - git.stdout.on("data", (data) => - { - data = data.toString().split('\n').filter((item) => item != ""); - data[0] = JSON.parse(data[0]); + const walker = git.Revwalk.create(repo); + walker.pushHead(); - ["files changed", "insertions", "deletions"].forEach((stat) => - { - const stat_nr = new RegExp(`(\\d+)\\ ${stat.replaceAll(/s(?=(\ |$))/g, "s?")}`).exec(data[1]); - data[0][stat.replaceAll(" ", "_")] = stat_nr ? stat_nr[1] : 0; - }); - - log.push(data[0]); - }); + const raw_commits = await walker.getCommits(); - git.on("close", (code) => - { - if(code === 0) { - resolve({ "data": log }); - return; - } - resolve({ "error": "Failed to communicate with git!" }); - }); - }) + 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() + }))); + + resolve(await commits); + }); } -function getTimeSinceLatestCommit(path) +function getTimeSinceLatestCommit(base_dir, repo_name) { - return new Promise((resolve) => + return new Promise(async (resolve) => { - const git = execGit(path, "log", [`--format=format:%at`, "-n 1"], (err) => resolve(err)); - - const commit = []; - - git.stdout.on("data", (data) => commit.push(data)); + const repo = await git.Repository.openBare(`${base_dir}/${repo_name}`) + const master_commit = await repo.getMasterCommit(); - git.on("close", (code) => - { - resolve(formatDistance(new Date(), new Date(Number(Buffer.concat(commit).toString()) * 1000))); - }); + resolve(formatDistance(new Date(), master_commit.date())); }); } @@ -80,7 +55,7 @@ function getRepoFile(base_dir, repo, file) fs.readFile(`${base_dir}/${repo}/${file}`, async (err, content) => { if(!err) { - resolve(content.toString().replaceAll('\n', '')); + resolve(content.toString().replace(/\n/g, "")); return; } resolve(""); @@ -88,222 +63,170 @@ function getRepoFile(base_dir, repo, file) }); } -function getBasicRepoInfo(base_dir, repo_dirs) -{ - return new Promise((resolve) => - { - let repos = {}; - repo_dirs.forEach(async (repo, index, arr) => - { - const desc = await getRepoFile(base_dir, repo, "description"); - const owner = await getRepoFile(base_dir, repo, "owner"); - const last_commit_date = await getTimeSinceLatestCommit(`${base_dir}/${repo}`); - - let repo_name = ""; - repo_name = repo.slice(0, -4); - repos[repo_name] = { "description": desc, "owner": owner, "last_updated": last_commit_date }; - - if(index === 0) resolve(repos); - }); - }); -} - function getRepos(base_dir) { return new Promise((resolve) => { - fs.readdir(base_dir, async (err, content) => + fs.readdir(base_dir, async (err, dir_content) => { if(err) { resolve({ "error": err }); return; } - resolve({ "data": content }); + + dir_content = dir_content.filter(repo => repo.endsWith(".git")); + + let repos = {}; + + dir_content.forEach(async (repo, index) => + { + const desc = await getRepoFile(base_dir, repo, "description"); + const owner = await getRepoFile(base_dir, repo, "owner"); + const last_commit_date = await getTimeSinceLatestCommit(base_dir, repo); + + let repo_name = ""; + repo_name = repo.slice(0, -4); + repos[repo_name] = { "description": desc, "owner": owner, "last_updated": last_commit_date }; + + if(index === 0) { + resolve(repos); + } + }); }); }); } -function parseCommitFilePart(part) +function parseHunkAddDel(hunk) { let new_lines = []; let deleted_lines = []; - let old_from; - let old_to; - let from; - let to; - if(/^@@\ -[0-9,]+\ \+[0-9,]+\ @@/.test(part[0])) { - const from_to = /^@@\ (-[0-9,]+)\ (\+[0-9,]+)\ @@(?:\ (.*))?/.exec(part[0]); - - old_from = Number(from_to[1].split(',')[0].slice(1)); - old_to = Number(from_to[1].split(',')[1]); - - from = Number(from_to[2].split(',')[0].slice(1)); - to = Number(from_to[2].split(',')[1]); - - if(old_from === 1 || from === 1) { - part = part.slice(1); - } - } - else { - old_from = 1; - old_to = part.length - new_lines.length; - - from = 1; - to = part.length - deleted_lines.length; - } - - part.forEach((line, index) => + hunk.forEach((line, index) => { if(line.charAt(0) === '+') { - line = line.slice(1); + hunk[index] = line.slice(1); new_lines.push(index); } else if(line.charAt(0) === '-') { - line = line.slice(1); + hunk[index] = line.slice(1); deleted_lines.push(index); } - else { - ["+", "-"].forEach((char) => - { - const find_char = new RegExp(`(?<=^<span.*>)\\${char}(?=.*<\/span>)`); - if(find_char.test(line)) { - console.log(`${char} ${line}`); - const char_index = find_char.exec(line)["index"]; - line = line.slice(0, char_index) + line.slice(char_index + 1) - if(char === "+") { - new_lines.push(index); - } - else if(char === "-") { - deleted_lines.push(index); - } - } - }) - } - part[index] = line; }); - return { "new_lines": new_lines, "deleted_lines": deleted_lines, "old_from": old_from, "old_to": old_to, "from": from, "to": to, "part": part.join("\n") }; + return { new: new_lines, deleted: deleted_lines, hunk: hunk.join("\n") }; } -function getCommit(base_dir, repo, hash) +function getCommit(base_dir, repo_name, commit_oid) { - return new Promise((resolve) => + return new Promise(async (resolve) => { - const git = execGit(`${base_dir}/${repo}`, "show", ['--format=format:{\"hash\": \"%H\", \"author\": \"%an <%ae>\", \"date\": \"%at\", \"message\": \"%s\"}', hash], (err) => resolve(err)); + repo_name = addRepoDirSuffix(repo_name); - let commit = []; + 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'); - git.stdout.on("data", (data) => + // 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) => { - commit.push(data); - }); + 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])); - git.on("close", () => - { - let diff = commit.toString().split('\n').slice(1); - - var result = []; + console.log(patch_header_data); - let start; - diff.forEach((line, index) => + const patches = await diff.patches(); + const test = patches.reduce((acc, patch, patch_index) => + { + return acc.then((arr) => { - if(/^diff\ --git a\/[^\ ]+\ b\/[^\ ]+$/.test(line) || index === diff.length - 1) { - if(start != undefined) { - let file_diff = diff.slice(start, index); - let chunk_header_index = file_diff.findIndex((line) => /^@@\ -[0-9,]+\ \+[0-9,]+\ @@/.test(line)); - if(chunk_header_index === -1) { - chunk_header_index = file_diff.length; + return patch.hunks().then((hunks) => + { + console.log(patch.newFile().path()); + + // 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 = all_patches.indexOf(hunk_header.replace(/\n/g, "")); + const hunk_header_index = patch_header_data[0][patch_index] + hunks_data[2] + patch_header_data[1][patch_index]; + + if(hunks_data[0] !== undefined) { + const prev_hunk = hunks[hunk_index - 1]; + //console.log(all_patches.slice(last_index, hunk_header_index)); + 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(all_patches.slice(hunks_data[0], hunk_header_index)))); + + hunks_data[2] = hunks_data + all_patches.slice(hunks_data[0], hunk_header_index).length; + } + + hunks_data[0] = hunk_header_index; + return hunks_data; + }); + }, Promise.resolve([undefined, [], 0])).then((hunks_data) => + { + console.log(" Patch start: " + hunks_data[0] + " " + all_patches[hunks_data[0]]); + + const patch_end = (patch_header_data[0][patch_index + 1] !== undefined) ? patch_header_data[0][patch_index + 1] : all_patches.length; + console.log(" Patch end: " + patch_end + " " + all_patches[patch_end]); + + 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(all_patches.slice(hunks_data[0], patch_end)))); + + if(patch_index === 8 || patch_index === 7) { + console.log(all_patches.slice(hunks_data[0], patch_end)); } - - let file_info = {}; - let header; - console.log(file_diff.join("\n")); - if(chunk_header_index != file_diff.length) { - const from_to = file_diff.slice(chunk_header_index - 2, chunk_header_index); - file_info["from"] = from_to[0].slice(4); - file_info["to"] = from_to[1].slice(4); - - const chunk_header = /^@@\ (-[0-9,]+)\ (\+[0-9,]+)\ @@(?:\ (.*))?/.exec(file_diff[chunk_header_index]); - - file_info["from_file_range"] = chunk_header[1]; - file_info["to_file_range"] = chunk_header[2]; - - let raw_diff = file_diff.slice(chunk_header_index); - let parsed_diff = []; - - let last_diff_start = 0; - raw_diff.forEach((diff_line, diff_index) => - { - console.log(raw_diff.length + " " + diff_index + " " + diff_line); - if(/^@@\ -[0-9,]+\ \+[0-9,]+\ @@/.test(diff_line) && diff_index !== 0) { - let part = parseCommitFilePart(raw_diff.slice(last_diff_start, diff_index)); - parsed_diff.push(part); - last_diff_start = diff_index; - } - else if(diff_index === raw_diff.length - 1) { - let part = parseCommitFilePart(raw_diff.slice(last_diff_start, diff_index + 1)); - parsed_diff.push(part); - } - }); - - console.log(raw_diff); - - file_info["diff"] = parsed_diff; + arr.push({ from: patch.oldFile().path(), to: patch.newFile().path(), hunks: hunks_data[1] }); + return arr; + }); + }); + }); + }, Promise.resolve([])); - header = file_diff.slice(1, chunk_header_index - 2); - } - else { - const from_to = /^diff\ --git (a\/[^\ ]+)\ (b\/[^\ ]+)$/.exec(file_diff[0]); - file_info["from"] = from_to[1]; - file_info["to"] = from_to[2]; - header = file_diff.slice(1, chunk_header_index); - } - - header.forEach((line) => - { - if(line.includes("old mode") || line.includes("new mode") || line.includes("deleted file mode") || line.includes("new file mode")) { - const data = /^(.*mode)\ (\d{6})$/.exec(line); - file_info[data[1].replaceAll(' ', "_")] = data[2]; - } - else if(line.includes("copy from") || line.includes("copy to")) { - const data = /^(copy\ from|to)\ (.*)/.exec(line); - file_info[data[1].replaceAll(' ', "_")] = data[2]; - } - else if(line.includes("rename from") || line.includes("rename to")) { - const data = /^(rename\ from|to)\ (.*)/.exec(line); - file_info[data[1].replaceAll(' ', "_")] = data[2]; - } - else if(line.includes("similarity index") || line.includes("dissimilarity index")) { - const data = /^((?:dis)?similarity\ index)\ (\d+%)$/.exec(line); - file_info[data[1].replaceAll(' ', "_")] = data[2]; - } - else if(line.includes("index")) { - const data = /^index\ ([0-9a-f,]+)\.\.([0-9a-f,]+)(?:\ ([0-9,]+))?$/.exec(line).slice(1); - file_info["index"] = { "before": data[0], "after": data[1] }; - if(data[2]) { - file_info["index"]["mode"] = data[2]; - } - } - }); - result.push(file_info); - } - start = index; - } - if(index === diff.length - 1) { - let data = JSON.parse(commit.toString().split('\n').slice(0,1)[0]); - data["files"] = result; - resolve({ "data": data }); - } + test.then((result) => + { + resolve({ + hash: commit.sha(), + author: commit.author().toString(), + message: commit.message(), + date: commit.date(), + patches: result }); + //console.log(result); }); - }) + }); } module.exports.getLog = getLog; -module.exports.getBasicRepoInfo = getBasicRepoInfo; module.exports.getRepos = getRepos; module.exports.getRepoFile = getRepoFile; module.exports.getCommit = getCommit;
\ No newline at end of file @@ -12,15 +12,13 @@ router.get("/info", function(req, res) router.get("/repos", async function(req, res) { - let repo_dirs = await git.getRepos(req.settings["base_dir"]); + let repos = await git.getRepos(req.settings["base_dir"]); - if(repo_dirs["error"]) { + if(repos["error"]) { res.status(500).send("Internal server error!"); return; } - repo_dirs = repo_dirs["data"].filter(repo => repo.endsWith(".git")); - const repos = await git.getBasicRepoInfo(req.settings["base_dir"], repo_dirs); res.json({ "data": repos }); }); @@ -58,7 +56,7 @@ router.get("/repos/:repo/log", async function(req, res) } return; } - res.json(log); + res.json({ data: log }); }); router.get("/repos/:repo/log/:commit", async function(req, res) @@ -70,7 +68,7 @@ router.get("/repos/:repo/log/:commit", async function(req, res) const commit = await git.getCommit(req.settings["base_dir"], req.params.repo, req.params.commit); - res.json(commit); + res.json({ data: commit }); }); module.exports = router;
\ No newline at end of file @@ -4,6 +4,7 @@ const git = require("./api/git"); const yaml = require('js-yaml'); const fs = require('fs'); const { exit } = require("process"); +const sanitization = require("./api/sanitization"); let settings; @@ -42,18 +43,28 @@ app.use("/api/v1", (req, res, next) => app.use("/:repo", async (req, res, next) => { - let repo_dirs = await git.getRepos(settings["base_dir"]); - - if(repo_dirs["error"]) { - res.status(500).send("Internal server error!"); + if(!sanitization.sanitizeRepoName(req.params.repo)) { + res.status(400).json({ "error": "Unacceptable git repository name!" }); return; } - if(!repo_dirs["data"].includes(req.params.repo)) { - res.status(404).send("404: Page not found"); - return; - } - next(); + fs.readdir(settings["base_dir"], (err, dir_content) => + { + if(err) { + res.status(404).send("404: Page not found"); + return; + } + + dir_content = dir_content.filter(repo => repo.endsWith(".git")); + + if(!dir_content.includes(req.params.repo + ".git")) { + res.status(404).send("404: Page not found"); + return; + } + else { + next(); + } + }); }) app.get("/:repo", (req, res) => @@ -72,10 +83,9 @@ app.get("/:repo/:page", (req, res, next) => res.sendFile("dist/app.html", { root: __dirname }); }); -app.get("/:repo/log/:hash", (req, res, next) => +app.get("/:repo/log/:commit", (req, res, next) => { - const valid_hash = /^[0-9a-f]+$/; - if(!valid_hash.test(req.params.hash)) { + if(!sanitization.sanitizeCommitID(req.params.commit)) { next(); return; } |