aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2021-05-19 15:59:12 +0200
committerHampusM <hampus@hampusmat.com>2021-05-19 15:59:12 +0200
commite82d9498d836ea100a186c4d87a7d0314ec1693a (patch)
treee48f2c7a230aafe6ff91ade54cdee8fad7ec2986
parentfcc482fd61077a6af6cd9fcfad6f75c72aa59355 (diff)
Backend uses nodegit, gettings repos is a single function & backend cleaned up
-rw-r--r--api/git.js379
-rw-r--r--api/v1.js10
-rw-r--r--app.js34
3 files changed, 177 insertions, 246 deletions
diff --git a/api/git.js b/api/git.js
index 80cec1a..d58507f 100644
--- a/api/git.js
+++ b/api/git.js
@@ -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
diff --git a/api/v1.js b/api/v1.js
index 4987ba7..7920afc 100644
--- a/api/v1.js
+++ b/api/v1.js
@@ -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
diff --git a/app.js b/app.js
index 1a6f8ab..ed21334 100644
--- a/app.js
+++ b/app.js
@@ -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;
}