diff --git a/git-serve-server.js b/git-serve-server.js new file mode 100644 index 00000000..3a51bb1a --- /dev/null +++ b/git-serve-server.js @@ -0,0 +1,66 @@ +// @ts-check + +const https = require('https'); +const http = require('http'); +const fs = require('fs'); + +const gitstatic = require("./git-serve"); +const express = require("express"); + +const repository = '.git'; + +const app = express(); +app.get(/^\/.+/, gitstatic.route().repository(repository)); +app.get(/\//, (req, res) => { + gitstatic.listAllCommits(repository, (err, commits) => { + console.log(err, commits); + + res.send( + commits.map((commit) => { + return `${commit.sha.slice(0, 7)} - ${commit.date.toISOString()} - ${commit.subject}
`; + }).join('') + ); + }); +}); + +const { networkInterfaces } = require('os'); +const nets = networkInterfaces(); +const results = {}; + +for(const name of Object.keys(nets)) { + for(const net of nets[name]) { + // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses + if(net.family === 'IPv4' && !net.internal) { + if(!results[name]) { + results[name] = []; + } + results[name].push(net.address); + } + } +} + +const useHttp = false; +const server = useHttp ? http : https; +let options = {}; +if(!useHttp) { + options.key = fs.readFileSync(__dirname + '/certs/server-key.pem'); + options.cert = fs.readFileSync(__dirname + '/certs/server-cert.pem'); +} + +const port = 3000; +const protocol = useHttp ? 'http' : 'https'; +console.log('Listening port:', port); +function createServer(host) { + server.createServer(options, app).listen(port, host, () => { + console.log('Host:', `${protocol}://${host || 'localhost'}:${port}/`); + }); +} + +for(const name in results) { + const ips = results[name]; + for(const ip of ips) { + createServer(ip); + } +} + +createServer(); diff --git a/git-serve.js b/git-serve.js new file mode 100644 index 00000000..fa5d2017 --- /dev/null +++ b/git-serve.js @@ -0,0 +1,246 @@ +// @ts-check + +// Thanks to https://github.com/mbostock/git-static + +var child = require("child_process"), + mime = require("mime"), + path = require("path"); + +var shaRe = /^[0-9a-f]{40}$/, + emailRe = /^<.*@.*>$/; + +function readBlob(repository, revision, file, callback) { + var git = child.spawn("git", ["cat-file", "blob", revision + ":" + file], {cwd: repository}), + data = [], + exit; + + git.stdout.on("data", function(chunk) { + data.push(chunk); + }); + + git.on("exit", function(code) { + exit = code; + }); + + git.on("close", function() { + if (exit > 0) return callback(error(exit)); + callback(null, Buffer.concat(data)); + }); + + git.stdin.end(); +} + +exports.readBlob = readBlob; + +exports.getBranches = function(repository, callback) { + child.exec("git branch -l", {cwd: repository}, function(error, stdout) { + if (error) return callback(error); + callback(null, stdout.split(/\n/).slice(0, -1).map(function(s) { return s.slice(2); })); + }); +}; + +exports.getSha = function(repository, revision, callback) { + child.exec("git rev-parse '" + revision.replace(/'/g, "'\''") + "'", {cwd: repository}, function(error, stdout) { + if (error) return callback(error); + callback(null, stdout.trim()); + }); +}; + +exports.getBranchCommits = function(repository, callback) { + child.exec("git for-each-ref refs/heads/ --sort=-authordate --format='%(objectname)\t%(refname:short)\t%(authordate:iso8601)\t%(authoremail)'", {cwd: repository}, function(error, stdout) { + if (error) return callback(error); + callback(null, stdout.split("\n").map(function(line) { + var fields = line.split("\t"), + sha = fields[0], + ref = fields[1], + date = new Date(fields[2]), + author = fields[3]; + if (!shaRe.test(sha) || !date || !emailRe.test(author)) return; + return { + sha: sha, + ref: ref, + date: date, + author: author.substring(1, author.length - 1) + }; + }).filter(function(commit) { + return commit; + })); + }); +}; + +exports.getCommit = function(repository, revision, callback) { + if (arguments.length < 3) callback = revision, revision = null; + child.exec(shaRe.test(revision) + ? "git log -1 --date=iso " + revision + " --format='%H\n%ad'" + : "git for-each-ref --count 1 --sort=-authordate 'refs/heads/" + (revision ? revision.replace(/'/g, "'\''") : "") + "' --format='%(objectname)\n%(authordate:iso8601)'", {cwd: repository}, function(error, stdout) { + if (error) return callback(error); + var lines = stdout.split("\n"), + sha = lines[0], + date = new Date(lines[1]); + if (!shaRe.test(sha) || !date) return void callback(new Error("unable to get commit")); + callback(null, { + sha: sha, + date: date + }); + }); +}; + +exports.getRelatedCommits = function(repository, branch, sha, callback) { + if (!shaRe.test(sha)) return callback(new Error("invalid SHA: " + sha)); + child.exec("git log --format='%H' '" + branch.replace(/'/g, "'\''") + "' | grep -C1 " + sha, {cwd: repository}, function(error, stdout) { + if (error) return callback(error); + var shas = stdout.split(/\n/), + i = shas.indexOf(sha); + + callback(null, { + previous: shas[i + 1], + next: shas[i - 1] + }); + }); +}; + +exports.listCommits = function(repository, sha1, sha2, callback) { + if (!shaRe.test(sha1)) return callback(new Error("invalid SHA: " + sha1)); + if (!shaRe.test(sha2)) return callback(new Error("invalid SHA: " + sha2)); + child.exec("git log --format='%H\t%ad' " + sha1 + ".." + sha2, {cwd: repository}, function(error, stdout) { + if (error) return callback(error); + callback(null, stdout.split(/\n/).slice(0, -1).map(function(commit) { + var fields = commit.split(/\t/); + return { + sha: fields[0], + date: new Date(fields[1]) + }; + })); + }); +}; + +/** @type {(repository: string, callback: (err: Error, commits?: {sha: string, date: Date, author: string, subject: string}[]) => void) => void} */ +exports.listAllCommits = function(repository, callback) { + child.exec("git log --branches --format='%H\t%ad\t%an\t%s'", {cwd: repository}, function(error, stdout) { + if (error) return callback(error); + callback(null, stdout.split(/\n/).slice(0, -1).map(function(commit) { + var fields = commit.split(/\t/); + return { + sha: fields[0], + date: new Date(fields[1]), + author: fields[2], + subject: fields[3] + }; + })); + }); +}; + +exports.listTree = function(repository, revision, callback) { + child.exec("git ls-tree -r " + revision, {cwd: repository}, function(error, stdout) { + if (error) return callback(error); + callback(null, stdout.split(/\n/).slice(0, -1).map(function(commit) { + var fields = commit.split(/\t/); + return { + sha: fields[0].split(/\s/)[2], + name: fields[1] + }; + })); + }); +}; + +exports.route = function() { + var repository = defaultRepository, + revision = defaultRevision, + file = defaultFile, + type = defaultType; + + function route(request, response) { + var repository_, + revision_, + file_; + + // @ts-ignore + if ((repository_ = repository(request.url)) == null + || (revision_ = revision(request.url)) == null + || (file_ = file(request.url)) == null) return serveNotFound(); + + readBlob(repository_, revision_, file_, function(error, data) { + if (error) return error.code === 128 ? serveNotFound() : serveError(error); + response.writeHead(200, { + "Content-Type": type(file_), + "Cache-Control": "public, max-age=300" + }); + response.end(data); + }); + + function serveError(error) { + response.writeHead(500, {"Content-Type": "text/plain"}); + response.end(error + ""); + } + + function serveNotFound() { + response.writeHead(404, {"Content-Type": "text/plain"}); + response.end("File not found."); + } + } + + route.repository = function(_) { + if (!arguments.length) return repository; + repository = functor(_); + return route; + }; + + route.sha = // sha is deprecated; use revision instead + route.revision = function(_) { + if (!arguments.length) return revision; + revision = functor(_); + return route; + }; + + route.file = function(_) { + if (!arguments.length) return file; + file = functor(_); + return route; + }; + + route.type = function(_) { + if (!arguments.length) return type; + type = functor(_); + return route; + }; + + return route; +}; + +function functor(_) { + return typeof _ === "function" ? _ : function() { return _; }; +} + +function defaultRepository() { + return path.join(__dirname, "repository"); +} + +function defaultRevision(url) { + return decodeURIComponent(url.substring(1, url.indexOf("/", 1))); +} + +function defaultFile(url) { + url = url.substring(url.indexOf("/", 1) + 1); + const pathIdx = url.indexOf('?'); + if(pathIdx !== -1) { + url = url.slice(0, pathIdx); + } + + return decodeURIComponent(url); +} + +function defaultType(file) { + var type = mime.getType(file) || "text/plain"; + return text(type) ? type + "; charset=utf-8" : type; +} + +function text(type) { + return /^(text\/)|(application\/(javascript|json)|image\/svg$)/.test(type); +} + +function error(code) { + var e = new Error; + // @ts-ignore + e.code = code; + return e; +} diff --git a/package-lock.json b/package-lock.json index 76071c98..bc812070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "GPL-3.0-only", "dependencies": { + "mime": "^3.0.0", "webpack-dev-server": "^3.11.2" }, "devDependencies": { @@ -15390,14 +15391,14 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4.0.0" + "node": ">=10.0.0" } }, "node_modules/mime-db": { @@ -25914,6 +25915,17 @@ "readable-stream": "^2.0.1" } }, + "node_modules/webpack-dev-middleware/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/webpack-dev-server": { "version": "3.11.2", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz", @@ -39180,9 +39192,9 @@ } }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" }, "mime-db": { "version": "1.40.0", @@ -47230,6 +47242,11 @@ "errno": "^0.1.3", "readable-stream": "^2.0.1" } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" } } }, diff --git a/package.json b/package.json index 3e206eab..35cb5c5a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "author": "", "license": "GPL-3.0-only", "dependencies": { + "mime": "^3.0.0", "webpack-dev-server": "^3.11.2" }, "devDependencies": {