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": {