diff options
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/git.js | 237 | ||||
| -rw-r--r-- | src/api/sanitization.js | 14 | ||||
| -rw-r--r-- | src/api/v1.js | 74 | 
3 files changed, 325 insertions, 0 deletions
diff --git a/src/api/git.js b/src/api/git.js new file mode 100644 index 0000000..dad7cc6 --- /dev/null +++ b/src/api/git.js @@ -0,0 +1,237 @@ +const { formatDistance } = require('date-fns'); +const fs = require('fs'); +const git = require("nodegit"); + +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.getCommits(); +	 +	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 +	}; +} + +module.exports.getLog = getLog; +module.exports.getRepos = getRepos; +module.exports.getRepoFile = getRepoFile; +module.exports.getCommit = getCommit;
\ No newline at end of file diff --git a/src/api/sanitization.js b/src/api/sanitization.js new file mode 100644 index 0000000..95c8810 --- /dev/null +++ b/src/api/sanitization.js @@ -0,0 +1,14 @@ +function sanitizeRepoName(dirty) +{ +	const valid_repo_name = /^[a-zA-Z0-9\.\-_]+$/; +	return valid_repo_name.test(dirty); +} + +function sanitizeCommitID(dirty) +{ +	const valid_commit_id = /^[a-fA-F0-9]{40}$/; +	return valid_commit_id.test(dirty); +} + +module.exports.sanitizeRepoName = sanitizeRepoName; +module.exports.sanitizeCommitID = sanitizeCommitID;
\ No newline at end of file diff --git a/src/api/v1.js b/src/api/v1.js new file mode 100644 index 0000000..7920afc --- /dev/null +++ b/src/api/v1.js @@ -0,0 +1,74 @@ +const express = require("express"); +const git = require("./git"); +const sanitization = require("./sanitization"); + +const router = express.Router(); + +router.get("/info", function(req, res) +{ +	res.json({ "data": req.settings }); +	return; +}); + +router.get("/repos", async function(req, res) +{ +	let repos = await git.getRepos(req.settings["base_dir"]); + +	if(repos["error"]) { +		res.status(500).send("Internal server error!"); +		return; +	} + +	res.json({ "data": repos }); +}); + +router.use("/repos/:repo", async function(req, res, next) +{ +	if(!sanitization.sanitizeRepoName(req.params.repo)) { +		res.status(400).json({ "error": "Unacceptable git repository name!" }); +		return; +	} +	next(); +}); + +router.get("/repos/:repo", async function(req, res) +{ +	const repo = `${req.params.repo}.git`; +	const desc = await git.getRepoFile(req.settings["base_dir"], repo, "description"); + +	res.json({ "data": { "name": req.params.repo, "description": desc } }); +}); + +router.get("/repos/:repo/log", async function(req, res) +{ +	const repo = `${req.params.repo}.git`; +	const log = await git.getLog(req.settings["base_dir"], repo); + +	if(log["error"]) { +		if(typeof log["error"] === "string") { +			res.status(500).json({ "error": log["error"] }); +			return; +		} +		switch(log["error"]) { +			case 404: +				res.status(404).json({ "error": "Git repository doesn't exist!" }); +				return; +		} +		return; +	} +	res.json({ data: log }); +}); + +router.get("/repos/:repo/log/:commit", async function(req, res) +{ +	if(!sanitization.sanitizeCommitID(req.params.commit)) { +		res.status(400).json({ "error": "Unacceptable commit id!" }); +		return; +	} + +	const commit = await git.getCommit(req.settings["base_dir"], req.params.repo, req.params.commit); + +	res.json({ data: commit }); +}); + +module.exports = router;
\ No newline at end of file  | 
