git-serve.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. // @ts-check
  2. // Thanks to https://github.com/mbostock/git-static
  3. var child = require("child_process"),
  4. mime = require("mime"),
  5. path = require("path");
  6. var shaRe = /^[0-9a-f]{40}$/,
  7. emailRe = /^<.*@.*>$/;
  8. function readBlob(repository, revision, file, callback) {
  9. var git = child.spawn("git", ["cat-file", "blob", revision + ":" + file], {cwd: repository}),
  10. data = [],
  11. exit;
  12. git.stdout.on("data", function(chunk) {
  13. data.push(chunk);
  14. });
  15. git.on("exit", function(code) {
  16. exit = code;
  17. });
  18. git.on("close", function() {
  19. if (exit > 0) return callback(error(exit));
  20. callback(null, Buffer.concat(data));
  21. });
  22. git.stdin.end();
  23. }
  24. exports.readBlob = readBlob;
  25. exports.getBranches = function(repository, callback) {
  26. child.exec("git branch -l", {cwd: repository}, function(error, stdout) {
  27. if (error) return callback(error);
  28. callback(null, stdout.split(/\n/).slice(0, -1).map(function(s) { return s.slice(2); }));
  29. });
  30. };
  31. exports.getSha = function(repository, revision, callback) {
  32. child.exec("git rev-parse '" + revision.replace(/'/g, "'\''") + "'", {cwd: repository}, function(error, stdout) {
  33. if (error) return callback(error);
  34. callback(null, stdout.trim());
  35. });
  36. };
  37. exports.getBranchCommits = function(repository, callback) {
  38. 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) {
  39. if (error) return callback(error);
  40. callback(null, stdout.split("\n").map(function(line) {
  41. var fields = line.split("\t"),
  42. sha = fields[0],
  43. ref = fields[1],
  44. date = new Date(fields[2]),
  45. author = fields[3];
  46. if (!shaRe.test(sha) || !date || !emailRe.test(author)) return;
  47. return {
  48. sha: sha,
  49. ref: ref,
  50. date: date,
  51. author: author.substring(1, author.length - 1)
  52. };
  53. }).filter(function(commit) {
  54. return commit;
  55. }));
  56. });
  57. };
  58. exports.getCommit = function(repository, revision, callback) {
  59. if (arguments.length < 3) callback = revision, revision = null;
  60. child.exec(shaRe.test(revision)
  61. ? "git log -1 --date=iso " + revision + " --format='%H\n%ad'"
  62. : "git for-each-ref --count 1 --sort=-authordate 'refs/heads/" + (revision ? revision.replace(/'/g, "'\''") : "") + "' --format='%(objectname)\n%(authordate:iso8601)'", {cwd: repository}, function(error, stdout) {
  63. if (error) return callback(error);
  64. var lines = stdout.split("\n"),
  65. sha = lines[0],
  66. date = new Date(lines[1]);
  67. if (!shaRe.test(sha) || !date) return void callback(new Error("unable to get commit"));
  68. callback(null, {
  69. sha: sha,
  70. date: date
  71. });
  72. });
  73. };
  74. exports.getRelatedCommits = function(repository, branch, sha, callback) {
  75. if (!shaRe.test(sha)) return callback(new Error("invalid SHA: " + sha));
  76. child.exec("git log --format='%H' '" + branch.replace(/'/g, "'\''") + "' | grep -C1 " + sha, {cwd: repository}, function(error, stdout) {
  77. if (error) return callback(error);
  78. var shas = stdout.split(/\n/),
  79. i = shas.indexOf(sha);
  80. callback(null, {
  81. previous: shas[i + 1],
  82. next: shas[i - 1]
  83. });
  84. });
  85. };
  86. exports.listCommits = function(repository, sha1, sha2, callback) {
  87. if (!shaRe.test(sha1)) return callback(new Error("invalid SHA: " + sha1));
  88. if (!shaRe.test(sha2)) return callback(new Error("invalid SHA: " + sha2));
  89. child.exec("git log --format='%H\t%ad' " + sha1 + ".." + sha2, {cwd: repository}, function(error, stdout) {
  90. if (error) return callback(error);
  91. callback(null, stdout.split(/\n/).slice(0, -1).map(function(commit) {
  92. var fields = commit.split(/\t/);
  93. return {
  94. sha: fields[0],
  95. date: new Date(fields[1])
  96. };
  97. }));
  98. });
  99. };
  100. /** @type {(repository: string, callback: (err: Error, commits?: {sha: string, date: Date, author: string, subject: string}[]) => void) => void} */
  101. exports.listAllCommits = function(repository, callback) {
  102. child.exec("git log --branches --format='%H\t%ad\t%an\t%s'", {cwd: repository}, function(error, stdout) {
  103. if (error) return callback(error);
  104. callback(null, stdout.split(/\n/).slice(0, -1).map(function(commit) {
  105. var fields = commit.split(/\t/);
  106. return {
  107. sha: fields[0],
  108. date: new Date(fields[1]),
  109. author: fields[2],
  110. subject: fields[3]
  111. };
  112. }));
  113. });
  114. };
  115. exports.listTree = function(repository, revision, callback) {
  116. child.exec("git ls-tree -r " + revision, {cwd: repository}, function(error, stdout) {
  117. if (error) return callback(error);
  118. callback(null, stdout.split(/\n/).slice(0, -1).map(function(commit) {
  119. var fields = commit.split(/\t/);
  120. return {
  121. sha: fields[0].split(/\s/)[2],
  122. name: fields[1]
  123. };
  124. }));
  125. });
  126. };
  127. exports.route = function() {
  128. var repository = defaultRepository,
  129. revision = defaultRevision,
  130. file = defaultFile,
  131. type = defaultType;
  132. function route(request, response) {
  133. var repository_,
  134. revision_,
  135. file_;
  136. // @ts-ignore
  137. if ((repository_ = repository(request.url)) == null
  138. || (revision_ = revision(request.url)) == null
  139. || (file_ = file(request.url)) == null) return serveNotFound();
  140. readBlob(repository_, revision_, file_, function(error, data) {
  141. if (error) return error.code === 128 ? serveNotFound() : serveError(error);
  142. response.writeHead(200, {
  143. "Content-Type": type(file_),
  144. "Cache-Control": "public, max-age=300"
  145. });
  146. response.end(data);
  147. });
  148. function serveError(error) {
  149. response.writeHead(500, {"Content-Type": "text/plain"});
  150. response.end(error + "");
  151. }
  152. function serveNotFound() {
  153. response.writeHead(404, {"Content-Type": "text/plain"});
  154. response.end("File not found.");
  155. }
  156. }
  157. route.repository = function(_) {
  158. if (!arguments.length) return repository;
  159. repository = functor(_);
  160. return route;
  161. };
  162. route.sha = // sha is deprecated; use revision instead
  163. route.revision = function(_) {
  164. if (!arguments.length) return revision;
  165. revision = functor(_);
  166. return route;
  167. };
  168. route.file = function(_) {
  169. if (!arguments.length) return file;
  170. file = functor(_);
  171. return route;
  172. };
  173. route.type = function(_) {
  174. if (!arguments.length) return type;
  175. type = functor(_);
  176. return route;
  177. };
  178. return route;
  179. };
  180. function functor(_) {
  181. return typeof _ === "function" ? _ : function() { return _; };
  182. }
  183. function defaultRepository() {
  184. return path.join(__dirname, "repository");
  185. }
  186. function defaultRevision(url) {
  187. return decodeURIComponent(url.substring(1, url.indexOf("/", 1)));
  188. }
  189. function defaultFile(url) {
  190. url = url.substring(url.indexOf("/", 1) + 1);
  191. const pathIdx = url.indexOf('?');
  192. if(pathIdx !== -1) {
  193. url = url.slice(0, pathIdx);
  194. }
  195. return decodeURIComponent(url);
  196. }
  197. function defaultType(file) {
  198. var type = mime.getType(file) || "text/plain";
  199. return text(type) ? type + "; charset=utf-8" : type;
  200. }
  201. function text(type) {
  202. return /^(text\/)|(application\/(javascript|json)|image\/svg$)/.test(type);
  203. }
  204. function error(code) {
  205. var e = new Error;
  206. // @ts-ignore
  207. e.code = code;
  208. return e;
  209. }