aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2021-06-07 20:54:26 +0200
committerHampusM <hampus@hampusmat.com>2021-06-07 20:54:26 +0200
commit86395bd219b770133cb80d4bda4efc9155a4eef8 (patch)
treec07461a5da2e7ae4e8e29d930b4fbb63d2f509d3 /packages
parent6a89b265146edf689b83e2f0bf2d3d6d70eb538a (diff)
Refactored the backend. It's written in typescript now.
Diffstat (limited to 'packages')
-rw-r--r--packages/server/.eslintrc.js559
-rw-r--r--packages/server/src/api/git.js412
-rw-r--r--packages/server/src/api/git.ts395
-rw-r--r--packages/server/src/api/util.js45
-rw-r--r--packages/server/src/api/util.ts79
-rw-r--r--packages/server/src/api/v1.js136
-rw-r--r--packages/server/src/api/v1.ts123
-rw-r--r--packages/server/src/app.js98
-rw-r--r--packages/server/src/app.ts127
-rw-r--r--packages/server/tsconfig.json11
10 files changed, 1013 insertions, 972 deletions
diff --git a/packages/server/.eslintrc.js b/packages/server/.eslintrc.js
index 382a348..b9ebf8b 100644
--- a/packages/server/.eslintrc.js
+++ b/packages/server/.eslintrc.js
@@ -1,283 +1,280 @@
module.exports = {
- "env": {
- "commonjs": true,
- "es2021": true,
- "node": true
- },
- "extends": "eslint:recommended",
- "parserOptions": {
- "ecmaVersion": 12
- },
- "rules": {
- "accessor-pairs": "error",
- "array-bracket-newline": "error",
- "array-bracket-spacing": [
- "error",
- "always"
- ],
- "array-callback-return": "error",
- "array-element-newline": "off",
- "arrow-body-style": "error",
- "arrow-parens": [
- "error",
- "as-needed"
- ],
- "arrow-spacing": [
- "error",
- {
- "after": true,
- "before": true
- }
- ],
- "block-scoped-var": "error",
- "block-spacing": "error",
- "brace-style": "off",
- "camelcase": "off",
- "capitalized-comments": [
- "error",
- "always"
- ],
- "class-methods-use-this": "error",
- "comma-dangle": "error",
- "comma-spacing": [
- "error",
- {
- "after": true,
- "before": false
- }
- ],
- "comma-style": [
- "error",
- "last"
- ],
- "complexity": "error",
- "computed-property-spacing": [
- "error",
- "never"
- ],
- "consistent-return": "error",
- "consistent-this": "error",
- "curly": "error",
- "default-case": "error",
- "default-case-last": "error",
- "default-param-last": "error",
- "dot-location": "error",
- "dot-notation": "off",
- "eol-last": [
- "error",
- "never"
- ],
- "eqeqeq": "error",
- "func-call-spacing": "error",
- "func-name-matching": "error",
- "func-names": "off",
- "func-style": "error",
- "function-paren-newline": "error",
- "generator-star-spacing": "error",
- "grouped-accessor-pairs": "error",
- "guard-for-in": "error",
- "id-denylist": "error",
- "id-length": "off",
- "id-match": "error",
- "implicit-arrow-linebreak": [
- "error",
- "beside"
- ],
- "indent": "off",
- "init-declarations": "error",
- "jsx-quotes": "error",
- "key-spacing": "error",
- "keyword-spacing": "off",
- "line-comment-position": "error",
- "linebreak-style": [
- "error",
- "unix"
- ],
- "lines-around-comment": "error",
- "lines-between-class-members": "error",
- "max-classes-per-file": "error",
- "max-depth": "error",
- "max-len": "off",
- "max-lines": "error",
- "max-lines-per-function": "error",
- "max-nested-callbacks": "error",
- "max-params": "error",
- "max-statements": "error",
- "max-statements-per-line": "error",
- "multiline-comment-style": "error",
- "new-cap": "error",
- "new-parens": "error",
- "newline-per-chained-call": "error",
- "no-alert": "error",
- "no-array-constructor": "error",
- "no-await-in-loop": "error",
- "no-bitwise": "error",
- "no-caller": "error",
- "no-confusing-arrow": "error",
- "no-console": "off",
- "no-constructor-return": "error",
- "no-continue": "error",
- "no-div-regex": "error",
- "no-duplicate-imports": "error",
- "no-else-return": [
- "error",
- {
- "allowElseIf": true
- }
- ],
- "no-empty-function": "error",
- "no-eq-null": "error",
- "no-eval": "error",
- "no-extend-native": "error",
- "no-extra-bind": "error",
- "no-extra-label": "error",
- "no-extra-parens": "off",
- "no-floating-decimal": "error",
- "no-implicit-coercion": "error",
- "no-implicit-globals": "error",
- "no-implied-eval": "error",
- "no-inline-comments": "error",
- "no-invalid-this": "error",
- "no-iterator": "error",
- "no-label-var": "error",
- "no-labels": "error",
- "no-lone-blocks": "error",
- "no-lonely-if": "error",
- "no-loop-func": "error",
- "no-loss-of-precision": "error",
- "no-magic-numbers": "off",
- "no-mixed-operators": "error",
- "no-multi-assign": "error",
- "no-multi-spaces": "error",
- "no-multi-str": "error",
- "no-multiple-empty-lines": "error",
- "no-negated-condition": "error",
- "no-nested-ternary": "error",
- "no-new": "error",
- "no-new-func": "error",
- "no-new-object": "error",
- "no-new-wrappers": "error",
- "no-nonoctal-decimal-escape": "error",
- "no-octal-escape": "error",
- "no-param-reassign": "error",
- "no-plusplus": "error",
- "no-promise-executor-return": "error",
- "no-proto": "error",
- "no-restricted-exports": "error",
- "no-restricted-globals": "error",
- "no-restricted-imports": "error",
- "no-restricted-properties": "error",
- "no-restricted-syntax": "error",
- "no-return-assign": "error",
- "no-return-await": "error",
- "no-script-url": "error",
- "no-self-compare": "error",
- "no-sequences": "error",
- "no-shadow": "error",
- "no-tabs": [
- "error",
- {
- "allowIndentationTabs": true
- }
- ],
- "no-template-curly-in-string": "error",
- "no-ternary": "off",
- "no-throw-literal": "error",
- "no-undef-init": "error",
- "no-undefined": "error",
- "no-underscore-dangle": "error",
- "no-unmodified-loop-condition": "error",
- "no-unneeded-ternary": "error",
- "no-unreachable-loop": "error",
- "no-unsafe-optional-chaining": "error",
- "no-unused-expressions": "error",
- "no-use-before-define": "error",
- "no-useless-backreference": "error",
- "no-useless-call": "error",
- "no-useless-computed-key": "error",
- "no-useless-concat": "error",
- "no-useless-constructor": "error",
- "no-useless-rename": "error",
- "no-useless-return": "error",
- "no-var": "error",
- "no-void": "error",
- "no-warning-comments": "error",
- "no-whitespace-before-property": "error",
- "nonblock-statement-body-position": "error",
- "object-curly-newline": "error",
- "object-curly-spacing": [
- "error",
- "always"
- ],
- "object-shorthand": "off",
- "one-var": "off",
- "one-var-declaration-per-line": "error",
- "operator-assignment": "error",
- "operator-linebreak": "error",
- "padded-blocks": "off",
- "padding-line-between-statements": "error",
- "prefer-arrow-callback": "off",
- "prefer-const": "off",
- "prefer-destructuring": "error",
- "prefer-exponentiation-operator": "error",
- "prefer-named-capture-group": "error",
- "prefer-numeric-literals": "error",
- "prefer-object-spread": "error",
- "prefer-promise-reject-errors": "error",
- "prefer-regex-literals": "error",
- "prefer-rest-params": "error",
- "prefer-spread": "error",
- "prefer-template": "off",
- "quote-props": "off",
- "quotes": [ "error", "double" ],
- "radix": "error",
- "require-atomic-updates": "error",
- "require-await": "error",
- "require-unicode-regexp": "error",
- "rest-spread-spacing": "error",
- "semi": "error",
- "semi-spacing": "error",
- "semi-style": [
- "error",
- "last"
- ],
- "sort-imports": "error",
- "sort-keys": "off",
- "sort-vars": "error",
- "space-before-blocks": "error",
- "space-before-function-paren": "error",
- "space-in-parens": [
- "error",
- "never"
- ],
- "space-infix-ops": "error",
- "space-unary-ops": "error",
- "spaced-comment": [
- "error",
- "always"
- ],
- "strict": [
- "error",
- "never"
- ],
- "switch-colon-spacing": "error",
- "symbol-description": "error",
- "template-curly-spacing": [
- "error",
- "never"
- ],
- "template-tag-spacing": "error",
- "unicode-bom": [
- "error",
- "never"
- ],
- "vars-on-top": "error",
- "wrap-iife": "error",
- "wrap-regex": "error",
- "yield-star-spacing": "error",
- "yoda": [
- "error",
- "never"
- ]
- }
+ env: {
+ commonjs: true,
+ es2021: true,
+ node: true
+ },
+ extends: [ "standard" ],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ ecmaVersion: 12
+ },
+ plugins: [ "@typescript-eslint" ],
+ rules: {
+ "accessor-pairs": "error",
+ "array-bracket-newline": "error",
+ "array-bracket-spacing": [
+ "error",
+ "always"
+ ],
+ "array-callback-return": "error",
+ "array-element-newline": "off",
+ "arrow-body-style": "off",
+ "arrow-parens": [
+ "error",
+ "as-needed"
+ ],
+ "arrow-spacing": [
+ "error",
+ {
+ after: true,
+ before: true
+ }
+ ],
+ "block-scoped-var": "error",
+ "block-spacing": "error",
+ "brace-style": [ "error", "stroustrup", { "allowSingleLine": false } ],
+ camelcase: "off",
+ "capitalized-comments": [
+ "error",
+ "always"
+ ],
+ "class-methods-use-this": "error",
+ "comma-dangle": "error",
+ "comma-spacing": [
+ "error",
+ {
+ after: true,
+ before: false
+ }
+ ],
+ "comma-style": [
+ "error",
+ "last"
+ ],
+ complexity: "error",
+ "computed-property-spacing": [
+ "error",
+ "never"
+ ],
+ "consistent-return": "error",
+ "consistent-this": "error",
+ curly: [ "error", "all" ],
+ "default-case": "error",
+ "default-case-last": "error",
+ "default-param-last": "error",
+ "dot-location": "error",
+ "dot-notation": "off",
+ "eol-last": [
+ "error",
+ "never"
+ ],
+ eqeqeq: "error",
+ "func-call-spacing": "error",
+ "func-name-matching": "error",
+ "func-names": "off",
+ "func-style": [ "error", "declaration" ],
+ "function-paren-newline": "error",
+ "generator-star-spacing": "error",
+ "grouped-accessor-pairs": "error",
+ "guard-for-in": "error",
+ "id-denylist": "error",
+ "id-length": "off",
+ "id-match": "error",
+ "implicit-arrow-linebreak": [
+ "error",
+ "beside"
+ ],
+ indent: [ "warn", "tab" ],
+ "init-declarations": "error",
+ "jsx-quotes": "error",
+ "key-spacing": "error",
+ "keyword-spacing": "off",
+ "line-comment-position": "error",
+ "linebreak-style": [
+ "error",
+ "unix"
+ ],
+ "lines-around-comment": "error",
+ "lines-between-class-members": "error",
+ "max-classes-per-file": "error",
+ "max-depth": "error",
+ "max-len": "off",
+ "max-lines": [ "error", 600 ],
+ "max-lines-per-function": "error",
+ "max-nested-callbacks": "error",
+ "max-params": [ "error", 6 ],
+ "max-statements": "off",
+ "max-statements-per-line": "error",
+ "multiline-comment-style": "error",
+ "new-cap": "error",
+ "new-parens": "error",
+ "newline-per-chained-call": "error",
+ "no-alert": "error",
+ "no-array-constructor": "error",
+ "no-await-in-loop": "error",
+ "no-bitwise": "error",
+ "no-caller": "error",
+ "no-confusing-arrow": "error",
+ "no-console": "off",
+ "no-constructor-return": "error",
+ "no-continue": "error",
+ "no-div-regex": "error",
+ "no-duplicate-imports": "error",
+ "no-else-return": [
+ "error",
+ {
+ allowElseIf: true
+ }
+ ],
+ "no-empty-function": "error",
+ "no-eq-null": "error",
+ "no-eval": "error",
+ "no-extend-native": "error",
+ "no-extra-bind": "error",
+ "no-extra-label": "error",
+ "no-extra-parens": "off",
+ "no-floating-decimal": "error",
+ "no-implicit-coercion": "error",
+ "no-implicit-globals": "error",
+ "no-implied-eval": "error",
+ "no-inline-comments": "error",
+ "no-invalid-this": "error",
+ "no-iterator": "error",
+ "no-label-var": "error",
+ "no-labels": "error",
+ "no-lone-blocks": "error",
+ "no-lonely-if": "error",
+ "no-loop-func": "error",
+ "no-loss-of-precision": "error",
+ "no-magic-numbers": "off",
+ "no-mixed-operators": "error",
+ "no-multi-assign": "error",
+ "no-multi-spaces": "error",
+ "no-multi-str": "error",
+ "no-multiple-empty-lines": "error",
+ "no-negated-condition": "error",
+ "no-nested-ternary": "error",
+ "no-new": "error",
+ "no-new-func": "error",
+ "no-new-object": "error",
+ "no-new-wrappers": "error",
+ "no-nonoctal-decimal-escape": "error",
+ "no-octal-escape": "error",
+ "no-param-reassign": "error",
+ "no-plusplus": "error",
+ "no-promise-executor-return": "error",
+ "no-proto": "error",
+ "no-restricted-exports": "error",
+ "no-restricted-globals": "error",
+ "no-restricted-imports": "error",
+ "no-restricted-properties": "error",
+ "no-restricted-syntax": "error",
+ "no-return-assign": "error",
+ "no-return-await": "error",
+ "no-script-url": "error",
+ "no-self-compare": "error",
+ "no-sequences": "error",
+ "no-shadow": "error",
+ "no-tabs": "off",
+ "no-template-curly-in-string": "error",
+ "no-ternary": "off",
+ "no-throw-literal": "error",
+ "no-undef-init": "error",
+ "no-undefined": "error",
+ "no-underscore-dangle": "error",
+ "no-unmodified-loop-condition": "error",
+ "no-unneeded-ternary": "error",
+ "no-unreachable-loop": "error",
+ "no-unsafe-optional-chaining": "error",
+ "no-unused-expressions": "error",
+ "no-use-before-define": "error",
+ "no-useless-backreference": "error",
+ "no-useless-call": "error",
+ "no-useless-computed-key": "error",
+ "no-useless-concat": "error",
+ "no-useless-constructor": "error",
+ "no-useless-rename": "error",
+ "no-useless-return": "error",
+ "no-var": "error",
+ "no-void": "error",
+ "no-warning-comments": "error",
+ "no-whitespace-before-property": "error",
+ "nonblock-statement-body-position": "error",
+ "object-curly-newline": "error",
+ "object-curly-spacing": [
+ "error",
+ "always"
+ ],
+ "object-shorthand": "off",
+ "one-var": "off",
+ "one-var-declaration-per-line": "error",
+ "operator-assignment": "error",
+ "operator-linebreak": "error",
+ "padded-blocks": "off",
+ "padding-line-between-statements": "error",
+ "prefer-arrow-callback": "off",
+ "prefer-const": "off",
+ "prefer-destructuring": "off",
+ "prefer-exponentiation-operator": "error",
+ "prefer-named-capture-group": "error",
+ "prefer-numeric-literals": "error",
+ "prefer-object-spread": "error",
+ "prefer-promise-reject-errors": "error",
+ "prefer-regex-literals": "error",
+ "prefer-rest-params": "error",
+ "prefer-spread": "error",
+ "prefer-template": "off",
+ "quote-props": "off",
+ quotes: [ "error", "double" ],
+ radix: "error",
+ "require-atomic-updates": "error",
+ "require-await": "error",
+ "require-unicode-regexp": "error",
+ "rest-spread-spacing": "error",
+ semi: [ "error", "always" ],
+ "semi-spacing": "error",
+ "semi-style": [
+ "error",
+ "last"
+ ],
+ "sort-imports": "error",
+ "sort-keys": "off",
+ "sort-vars": "error",
+ "space-before-blocks": "error",
+ "space-before-function-paren": [ "error", "never" ],
+ "space-in-parens": [
+ "error",
+ "never"
+ ],
+ "space-infix-ops": "error",
+ "space-unary-ops": "error",
+ "spaced-comment": [
+ "error",
+ "always"
+ ],
+ strict: [
+ "error",
+ "never"
+ ],
+ "switch-colon-spacing": "error",
+ "symbol-description": "error",
+ "template-curly-spacing": [
+ "error",
+ "never"
+ ],
+ "template-tag-spacing": "error",
+ "unicode-bom": [
+ "error",
+ "never"
+ ],
+ "vars-on-top": "error",
+ "wrap-iife": "error",
+ "wrap-regex": "error",
+ "yield-star-spacing": "error",
+ yoda: [
+ "error",
+ "never"
+ ]
+ }
}; \ No newline at end of file
diff --git a/packages/server/src/api/git.js b/packages/server/src/api/git.js
deleted file mode 100644
index 80b808f..0000000
--- a/packages/server/src/api/git.js
+++ /dev/null
@@ -1,412 +0,0 @@
-const { formatDistance } = require('date-fns');
-const fs = require('fs');
-const git = require("nodegit");
-const zlib = require("zlib");
-const { spawn } = require('child_process');
-const whatwg = require("whatwg-url");
-const path = require("path");
-
-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.getCommitsUntil(() => true);
-
- 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) =>
- {
- 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
- };
-}
-
-async function doesCommitExist(base_dir, repo_name, commit_oid)
-{
- const repo = await git.Repository.openBare(`${base_dir}/${repo_name}`);
-
- try {
- await repo.getCommit(commit_oid);
- return true;
- }
- catch {
- return false;
- }
-}
-
-function connectToGitHTTPBackend(base_dir, req, reply)
-{
- const url_path = req.url.replace(req.params.repo, req.params.repo + ".git");
- const repo = req.params.repo + ".git";
- const repo_path = path.join(base_dir, repo);
-
- req = req.headers['Content-Encoding'] == 'gzip' ? req.pipe(zlib.createGunzip()) : req;
-
- const parsed_url = new whatwg.URL(`${req.protocol}://${req.hostname}${url_path}`);
- const url_path_parts = parsed_url.pathname.split('/');
-
- let service;
- let info = false;
-
- if(/\/info\/refs$/.test(parsed_url.pathname)) {
- service = parsed_url.searchParams.get("service");
- info = true;
- }
- else {
- service = url_path_parts[url_path_parts.length-1];
- }
-
- const content_type = `application/x-${service}-${info ? "advertisement" : "result"}`;
-
- if(/\.\/|\.\./.test(parsed_url.pathname)) {
- reply.header("Content-Type", content_type);
- reply.code(404).send("Git repository not found!\n");
- return;
- }
-
- if(service !== 'git-upload-pack') {
- reply.header("Content-Type", content_type);
- reply.code(403).send("Access denied!\n");
- return;
- }
-
- reply.raw.writeHead(200, { "Content-Type": content_type });
-
- const spawn_args = [ "--stateless-rpc", repo_path ];
-
- if(info) {
- spawn_args.push("--advertise-refs");
- }
-
- const git_pack = spawn(service, spawn_args);
-
- if(info) {
- const s = '# service=' + service + '\n';
- const n = (4 + s.length).toString(16);
- reply.raw.write(Buffer.from((Array(4 - n.length + 1).join('0') + n + s) + '0000'));
- }
- else {
- req.body.on("data", (data) => git_pack.stdin.write(data));
- req.body.on("close", () => git_pack.stdin.end());
- }
- git_pack.on("error", (err) => console.log(err));
- git_pack.stderr.on("data", (stderr) => console.log(stderr));
-
- git_pack.stdout.on("data", (data) =>
- {
- reply.raw.write(data);
- });
-
- git_pack.on("close", () => reply.raw.end());
-}
-
-async function getTree(base_dir, repo_name, tree_path)
-{
- repo_name = addRepoDirSuffix(repo_name);
-
- const repo = await git.Repository.openBare(`${base_dir}/${repo_name}`);
- const master_commit = await repo.getMasterCommit();
-
- const tree = await master_commit.getTree();
-
- let entries;
- if(tree_path) {
- try {
- const path_entry = await tree.getEntry(tree_path);
-
- if(path_entry.isBlob()) {
- return { type: "blob", content: (await path_entry.getBlob()).content().toString() };
- }
-
- entries = await (await path_entry.getTree()).entries();
- }
- catch(err) {
- if(err.errno === -3) {
- return { error: 404 };
- }
- return { error: 500 };
- }
- }
- else {
- entries = tree.entries();
- }
-
- return { type: "tree", tree: await entries.reduce((acc, entry) =>
- {
- return acc.then((obj) =>
- {
- return getTreeEntryLastCommit(repo, entry).then((last_commit) =>
- {
- obj[path.parse(entry.path()).base] = {
- oid: entry.oid(),
- type: entry.isBlob() ? "blob" : "tree",
- last_commit: {
- id: last_commit.id,
- message: last_commit.message,
- time: last_commit.time
- }
- };
- return obj;
- });
- });
- }, Promise.resolve({})) };
-}
-
-async function getTreeEntryLastCommit(repo, tree_entry)
-{
- const walker = git.Revwalk.create(repo);
- walker.pushHead();
-
- const raw_commits = await walker.getCommitsUntil(() => true);
-
- return raw_commits.reduce((acc, commit) =>
- {
- return acc.then((obj) =>
- {
- if(Object.keys(obj).length == 0) {
- return commit.getDiff().then((diffs) =>
- {
- return diffs[0].patches().then((patches) =>
- {
- let matching_path_patch;
- if(tree_entry.isBlob()) {
- matching_path_patch = patches.find((patch) => patch.newFile().path() === tree_entry.path());
- }
- else {
- matching_path_patch = patches.find((patch) => path.parse(patch.newFile().path()).dir.startsWith(tree_entry.path()));
- }
-
- if(matching_path_patch) {
- obj.id = commit.sha();
- obj.message = commit.message().replace(/\n/g, "");
- obj.time = commit.date();
- }
- return obj;
- });
- });
- }
-
- return obj;
- });
- }, Promise.resolve({}));
-}
-
-module.exports.getLog = getLog;
-module.exports.getRepos = getRepos;
-module.exports.getRepoFile = getRepoFile;
-module.exports.getCommit = getCommit;
-module.exports.doesCommitExist = doesCommitExist;
-module.exports.connectToGitHTTPBackend = connectToGitHTTPBackend;
-module.exports.getTree = getTree; \ No newline at end of file
diff --git a/packages/server/src/api/git.ts b/packages/server/src/api/git.ts
new file mode 100644
index 0000000..1f39692
--- /dev/null
+++ b/packages/server/src/api/git.ts
@@ -0,0 +1,395 @@
+import { ConvenientHunk, Repository, Revwalk, TreeEntry } from "nodegit";
+import { join, parse } from "path";
+import { readFile, readdir } from "fs";
+import { FastifyRequest } from "fastify";
+import { IncomingMessage } from "http";
+import { URL } from "whatwg-url";
+import { formatDistance } from "date-fns";
+import { spawn } from "child_process";
+import { verifyGitRequest } from "./util";
+
+function addRepoDirSuffix(repo_name: string) {
+ return repo_name.endsWith(".git") ? repo_name : `${repo_name}.git`;
+}
+
+function parseHunkAddDel(hunk: string[]) {
+ interface Lines {
+ new_lines: number[],
+ deleted_lines: number[]
+ }
+
+ const lines = hunk.reduce((lines_obj: Lines, line, index) => {
+ if(line.charAt(0) === "+") {
+ hunk[index] = line.slice(1);
+ lines_obj.new_lines.push(index);
+ }
+ else if(line.charAt(0) === "-") {
+ hunk[index] = line.slice(1);
+ lines_obj.deleted_lines.push(index);
+ }
+ return lines_obj;
+ }, { new_lines: [], deleted_lines: [] });
+
+ return Object.assign(lines, { hunk: hunk.join("\n") });
+}
+
+function getPatchHeaderData(patch_headers: string[], all_patches: string[]) {
+ interface PatchHeaderData {
+ indexes: number[],
+ lengths: number[],
+ last: number | null
+ };
+
+ return patch_headers.reduce((patch_header_data, line, index) => {
+ // The start of a patch header
+ if((/^diff --git/u).test(line)) {
+ patch_header_data.indexes.push(all_patches.indexOf(line));
+
+ if(patch_header_data.last !== null) {
+ patch_header_data.lengths.push(patch_headers.slice(patch_header_data.last, index).length);
+ }
+ patch_header_data.last = index;
+ }
+
+ // Include the last patch header when the end is reached
+ if(index === patch_headers.length - 1 && patch_header_data.last !== null) {
+ patch_header_data.lengths.push(patch_headers.slice(patch_header_data.last, index).length);
+ }
+
+ return patch_header_data;
+ }, <PatchHeaderData>{ indexes: [], lengths: [], last: null });
+}
+
+function getHunksData(hunks: ConvenientHunk[], patch_content: string[]) {
+ return hunks.reduce((hunks_data, hunk, hunk_index) => {
+ const hunk_header = hunk.header();
+ const hunk_header_index = patch_content.indexOf(hunk_header.replace(/\n/gu, ""));
+
+ if(hunks_data.prev !== null) {
+ const prev_hunk = hunks[hunk_index - 1];
+ hunks_data.hunks.push({
+ 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.prev, hunk_header_index))
+ });
+ }
+
+ hunks_data.prev = hunk_header_index;
+ return hunks_data;
+ }, { prev: null, hunks: [] });
+}
+
+function Patch(patch, too_large, hunks) {
+ this.from = patch.oldFile().path();
+ this.to = patch.newFile().path();
+ this.additions = patch.lineStats()["total_additions"];
+ this.deletions = patch.lineStats()["total_deletions"];
+ this.too_large = too_large;
+ this.hunks = hunks;
+}
+
+export type GitRequestInfo = {
+ repo: string,
+ url_path: string,
+ parsed_url: URL,
+ url_path_parts: string[],
+ is_discovery: boolean,
+ service: string | null,
+ content_type: string
+};
+
+function extractRequestInfo(req: FastifyRequest): GitRequestInfo {
+ const params: any = req.params;
+
+ const repo = params.repo + ".git";
+ const url_path = req.url.replace(params.repo, repo);
+
+ const parsed_url = new URL(`${req.protocol}://${req.hostname}${url_path}`);
+ const url_path_parts = parsed_url.pathname.split("/");
+
+ const is_discovery = (/\/info\/refs$/u).test(parsed_url.pathname);
+
+ const service = is_discovery ? parsed_url.searchParams.get("service") : url_path_parts[url_path_parts.length - 1];
+
+ const content_type = `application/x-${service}-${is_discovery ? "advertisement" : "result"}`;
+
+ return {
+ repo,
+ url_path,
+ parsed_url,
+ is_discovery,
+ url_path_parts,
+ service,
+ content_type
+ };
+}
+
+async function getTreeEntryLastCommit(repo: Repository, tree_entry: TreeEntry) {
+ const walker = Revwalk.create(repo);
+ walker.pushHead();
+
+ interface LastTreeEntryCommit {
+ id: string | null,
+ message: string | null,
+ date: Date | null
+ };
+
+ const raw_commits = await walker.getCommitsUntil(() => true);
+
+ return raw_commits.reduce((acc, commit) => acc.then(obj => {
+ if(obj.id === null) {
+ return commit.getDiff().then(diffs => diffs[0].patches().then(patches => {
+ let matching_path_patch = null;
+ if(tree_entry.isBlob()) {
+ matching_path_patch = patches.find(patch => patch.newFile().path() === tree_entry.path());
+ }
+ else {
+ matching_path_patch = patches.find(patch => parse(patch.newFile().path()).dir.startsWith(tree_entry.path()));
+ }
+
+ if(matching_path_patch) {
+ obj.id = commit.sha();
+ obj.message = commit.message().replace(/\n/gu, "");
+ obj.date = commit.date();
+ }
+ return obj;
+ }));
+ }
+
+ return obj;
+ }), Promise.resolve(<LastTreeEntryCommit>{ id: null, message: null, date: null }));
+}
+
+export class Git {
+ base_dir: string;
+
+ constructor(base_dir: string) {
+ this.base_dir = base_dir;
+ }
+
+ async getLog(repo_name: string) {
+ const full_repo_name = addRepoDirSuffix(repo_name);
+ const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`);
+
+ const walker: Revwalk = Revwalk.create(repo);
+ walker.pushHead();
+
+ const raw_commits = await walker.getCommitsUntil(() => true);
+
+ const commits = await 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/gu, ""),
+ 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 commits;
+ }
+
+ async getTimeSinceLatestCommit(repo_name: string) {
+ const full_repo_name = addRepoDirSuffix(repo_name);
+ const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`);
+
+ const master_commit = await repo.getMasterCommit();
+
+ return formatDistance(new Date(), master_commit.date());
+ }
+
+ getRepoFile(repo_name: string, file: string) {
+ return new Promise(resolve => {
+ const full_repo_name = addRepoDirSuffix(repo_name);
+ readFile(`${this.base_dir}/${full_repo_name}/${file}`, (err, content) => {
+ if(!err) {
+ resolve(content.toString().replace(/\n/gu, ""));
+ return;
+ }
+ resolve("");
+ });
+ });
+ }
+
+ getRepos() {
+ return new Promise(resolve => {
+ readdir(this.base_dir, (err: Error, dir_content: string[]) => {
+ if(err) {
+ resolve({ "error": err });
+ return;
+ }
+
+ dir_content.filter(repo => repo.endsWith(".git")).reduce((acc, repo) => {
+ return acc.then((repos: any) => {
+ return this.getRepoFile(repo, "description").then(description => {
+ return this.getRepoFile(repo, "owner").then(owner => {
+ return this.getTimeSinceLatestCommit(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);
+ });
+ });
+ });
+ }
+
+ async getCommit(repo_name: string, commit_oid: string) {
+ const full_repo_name = addRepoDirSuffix(repo_name);
+ const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`);
+ const commit = await repo.getCommit(commit_oid);
+ const diff = (await commit.getDiff())[0];
+ const all_patches = (await diff.toBuf(1)).toString().split("\n");
+ const patch_header_data = getPatchHeaderData((await diff.toBuf(2)).toString().split("\n"), all_patches);
+
+ const parsed_patches = (await diff.patches()).reduce((acc, patch, patch_index) => {
+ return acc.then(arr => patch.hunks().then(hunks => {
+ const patch_start = patch_header_data.indexes[patch_index] + patch_header_data.lengths[patch_index];
+ const patch_end = (typeof patch_header_data.indexes[patch_index + 1] === "undefined") ? all_patches.length - 1 : patch_header_data.indexes[patch_index + 1];
+ const patch_content = all_patches.slice(patch_start, patch_end);
+
+ const line_lengths = patch_content.map(line => line.length).reduce((result, length) => result + length);
+
+ if(patch_content.length > 5000 || line_lengths > 5000) {
+ console.log("Too large!");
+
+ arr.push(new Patch(patch, true, null));
+ return arr;
+ }
+
+ const hunks_data = getHunksData(hunks, patch_content);
+
+ const prev_hunk = hunks[hunks.length - 1];
+ hunks_data.hunks.push({
+ 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.prev, patch_end))
+ });
+
+ arr.push(new Patch(patch, false, hunks_data.hunks));
+
+ return arr;
+ }));
+ }, Promise.resolve([]));
+
+ return {
+ hash: commit.sha(),
+ author: commit.author().toString(),
+ message: commit.message(),
+ date: commit.date(),
+ patches: await parsed_patches
+ };
+ }
+
+ connectToGitHTTPBackend(req, reply) {
+ const request_info = extractRequestInfo(req);
+
+ const valid_request = verifyGitRequest(request_info);
+ if(valid_request.success === false) {
+ reply.header("Content-Type", request_info.content_type);
+ reply.code(valid_request.code).send(valid_request.message);
+ return;
+ }
+
+ reply.raw.writeHead(200, { "Content-Type": request_info.content_type });
+
+ const spawn_args = [ "--stateless-rpc", join(this.base_dir, request_info.repo) ];
+ if(request_info.is_discovery) {
+ spawn_args.push("--advertise-refs");
+ }
+
+ const git_pack = spawn(request_info.service, spawn_args);
+
+ if(request_info.is_discovery) {
+ const s = "# service=" + request_info.service + "\n";
+ const n = (4 + s.length).toString(16);
+ reply.raw.write(Buffer.from((Array(4 - n.length + 1).join("0") + n + s) + "0000"));
+ }
+ else {
+ const request_body: IncomingMessage = req.raw;
+
+ request_body.on("data", data => git_pack.stdin.write(data));
+ request_body.on("close", () => git_pack.stdin.end());
+ }
+
+ git_pack.on("error", err => console.log(err));
+
+ git_pack.stderr.on("data", (stderr: Buffer) => console.log(stderr.toString()));
+ git_pack.stdout.on("data", data => reply.raw.write(data));
+
+ git_pack.on("close", () => reply.raw.end());
+ }
+
+ async getTree(repo_name, tree_path) {
+ const full_repo_name = addRepoDirSuffix(repo_name);
+ const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`);
+ const master_commit = await repo.getMasterCommit();
+
+ const tree = await master_commit.getTree();
+
+ let entries = [];
+ if(tree_path) {
+ try {
+ const path_entry = await tree.getEntry(tree_path);
+
+ if(path_entry.isBlob()) {
+ return { type: "blob", content: (await path_entry.getBlob()).content().toString() };
+ }
+
+ entries = await (await path_entry.getTree()).entries();
+ }
+ catch(err) {
+ if(err.errno === -3) {
+ return { error: 404 };
+ }
+ return { error: 500 };
+ }
+ }
+ else {
+ entries = tree.entries();
+ }
+
+ return {
+ type: "tree",
+ tree: await entries.reduce((acc, entry) => {
+ return acc.then(obj => {
+ return getTreeEntryLastCommit(repo, entry).then(last_commit => {
+ obj[parse(entry.path()).base] = {
+ oid: entry.oid(),
+ type: entry.isBlob() ? "blob" : "tree",
+ last_commit: {
+ id: last_commit.id,
+ message: last_commit.message,
+ date: last_commit.date
+ }
+ };
+ return obj;
+ });
+ });
+ }, Promise.resolve({}))
+ };
+ }
+
+ async doesCommitExist(repo_name, commit_oid) {
+ const full_repo_name = addRepoDirSuffix(repo_name);
+ const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`);
+
+ try {
+ await repo.getCommit(commit_oid);
+ return true;
+ }
+ catch {
+ return false;
+ }
+ }
+}; \ No newline at end of file
diff --git a/packages/server/src/api/util.js b/packages/server/src/api/util.js
deleted file mode 100644
index aa31296..0000000
--- a/packages/server/src/api/util.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const fs = require("fs");
-const git = require("./git");
-
-function verifyRepoName(dirty, base_dir)
-{
- return new Promise((resolve) =>
- {
- const is_valid_repo_name = /^[a-zA-Z0-9\\.\-_]+$/.test(dirty);
- if(!is_valid_repo_name) {
- resolve("ERR_REPO_REGEX");
- }
-
- fs.readdir(base_dir, (err, dir_content) =>
- {
- if(err) {
- resolve("ERR_REPO_NOT_FOUND");
- }
-
- dir_content = dir_content.filter(repo => repo.endsWith(".git"));
- if(!dir_content.includes(dirty + ".git")) {
- resolve("ERR_REPO_NOT_FOUND");
- }
-
- resolve(true);
- });
- });
-}
-
-async function verifyCommitID(base_dir, repo, dirty)
-{
- if(!/^[a-fA-F0-9]+$/.test(dirty)) {
- return "ERR_COMMIT_REGEX";
- }
-
- const commit_exists = await git.doesCommitExist(base_dir, repo, dirty);
-
- if(!commit_exists) {
- return "ERR_COMMIT_NOT_FOUND";
- }
-
- return true;
-}
-
-module.exports.verifyRepoName = verifyRepoName;
-module.exports.verifyCommitID = verifyCommitID; \ No newline at end of file
diff --git a/packages/server/src/api/util.ts b/packages/server/src/api/util.ts
new file mode 100644
index 0000000..e4a7d17
--- /dev/null
+++ b/packages/server/src/api/util.ts
@@ -0,0 +1,79 @@
+import { Git, GitRequestInfo } from "./git";
+import { readdir } from "fs";
+
+type VerificationResultErrorType = "REPO_NOT_FOUND" | "REPO_INVALID" | "COMMIT_NOT_FOUND" | "COMMIT_INVALID" | "ACCESS_DENIED";
+
+const verification_error_types = {
+ REPO_NOT_FOUND: { code: 404, message: "Repository not found!" },
+ REPO_INVALID: { code: 403, message: "Invalid repository!" },
+ COMMIT_NOT_FOUND: { code: 404, message: "Commit not found!" },
+ COMMIT_INVALID: { code: 403, message: "Invalid commit!" },
+ ACCESS_DENIED: { code: 403, message: "Access denied!" }
+};
+
+export class VerificationResult {
+ constructor(success: boolean, error_type?: VerificationResultErrorType) {
+ this.success = success;
+
+ if(error_type) {
+ this.message = verification_error_types[error_type].message;
+ this.code = verification_error_types[error_type].code;
+ }
+ }
+
+ success: boolean;
+ code: number | null = null;
+ message: string | null = null;
+}
+
+export function verifyRepoName(base_dir: string, repo_name: string) {
+ return new Promise<VerificationResult>(resolve => {
+ console.log(repo_name);
+ const is_valid_repo_name = (/^[a-zA-Z0-9.\-_]+$/u).test(repo_name);
+ if(!is_valid_repo_name) {
+ resolve(new VerificationResult(false, "REPO_INVALID"));
+ return;
+ }
+
+ readdir(base_dir, (err: Error, dir_content: string[]) => {
+ if(err) {
+ resolve(new VerificationResult(false, "REPO_NOT_FOUND"));
+ return;
+ }
+
+ const dir_content_repos = dir_content.filter(repo => repo.endsWith(".git"));
+ if(!dir_content_repos.includes(repo_name + ".git")) {
+ resolve(new VerificationResult(false, "REPO_NOT_FOUND"));
+ return;
+ }
+
+ resolve(new VerificationResult(true));
+ });
+ });
+}
+
+export async function verifyCommitID(git: Git, repo: string, commit_id: string) {
+ if(!(/^[a-fA-F0-9]+$/u).test(commit_id)) {
+ return new VerificationResult(false, "COMMIT_INVALID");
+ }
+
+ const commit_exists = await git.doesCommitExist(repo, commit_id);
+
+ if(!commit_exists) {
+ return new VerificationResult(false, "COMMIT_NOT_FOUND");
+ }
+
+ return new VerificationResult(true);
+}
+
+export function verifyGitRequest(request_info: GitRequestInfo): VerificationResult {
+ if((/\.\/|\.\./u).test(request_info.parsed_url.pathname)) {
+ return new VerificationResult(false, "REPO_NOT_FOUND");
+ }
+
+ if(request_info.service !== "git-upload-pack") {
+ return new VerificationResult(false, "ACCESS_DENIED");
+ }
+
+ return new VerificationResult(true);
+} \ No newline at end of file
diff --git a/packages/server/src/api/v1.js b/packages/server/src/api/v1.js
deleted file mode 100644
index 25a8019..0000000
--- a/packages/server/src/api/v1.js
+++ /dev/null
@@ -1,136 +0,0 @@
-const git = require("./git");
-const util = require("./util");
-
-module.exports = function (fastify, opts, done)
-{
- fastify.route({
- method: "GET",
- path: "/info",
- handler: (req, reply) =>
- {
- reply.send({ data: opts.config.settings });
- }
- });
- fastify.route({
- method: "GET",
- path: "/repos",
- handler: async (req, reply) =>
- {
- let repos = await git.getRepos(opts.config.settings.base_dir);
-
- if(repos["error"]) {
- reply.code(500).send({ error: "Internal server error!" });
- return;
- }
-
- reply.send({ data: repos });
- }
- });
-
- fastify.route({
- method: "GET",
- path: "/repos/:repo",
- handler: async (req, reply) =>
- {
- const repo_verification = await util.verifyRepoName(req.params.repo, opts.config.settings.base_dir);
- if(repo_verification !== true) {
- if(repo_verification === "ERR_REPO_REGEX") {
- reply.code(400).send({ error: "Unacceptable git repository name!" });
- }
- else if(repo_verification === "ERR_REPO_NOT_FOUND") {
- reply.code(404).send({ error: "Git repository not found!" });
- }
- }
-
- const repo = `${req.params.repo}.git`;
- const desc = await git.getRepoFile(opts.config.settings.base_dir, repo, "description");
-
- reply.send({ data: { name: req.params.repo, description: desc } });
- }
- });
-
- fastify.register((fastify_repo, opts_repo, done_repo) =>
- {
- fastify_repo.addHook("onRequest", async (req, reply) =>
- {
- const repo_verification = await util.verifyRepoName(req.params.repo, opts.config.settings.base_dir);
- if(repo_verification !== true) {
- if(repo_verification === "ERR_REPO_REGEX") {
- reply.code(400).send({ error: "Unacceptable git repository name!" });
- }
- else if(repo_verification === "ERR_REPO_NOT_FOUND") {
- reply.code(404).send({ error: "Git repository not found!" });
- }
- }
- });
-
- fastify_repo.route({
- method: "GET",
- path: "/log",
- handler: async (req, reply) =>
- {
- const log = await git.getLog(opts.config.settings.base_dir, req.params.repo + ".git");
-
- if(log["error"]) {
- if(typeof log["error"] === "string") {
- reply.code(500).send({ error: log["error"] });
- }
-
- switch(log["error"]) {
- case 404:
- reply.code(404).send({ error: "Git repository not found!" });
- }
-
- return;
- }
- reply.send({ data: log });
- }
- });
-
- fastify_repo.route({
- method: "GET",
- path: "/log/:commit",
- handler: async (req, reply) =>
- {
- const commit_verification = await util.verifyCommitID(opts.config.settings.base_dir, req.params.repo + ".git", req.params.commit);
- if(!commit_verification !== true) {
- if(commit_verification === "ERR_COMMIT_REGEX") {
- reply.code(400).send({ error: "Unacceptable commit id!" });
- }
- else if(commit_verification === "ERR_COMMIT_NOT_FOUND") {
- reply.code(404).send({ error: "Commit not found!" });
- }
- }
-
- const commit = await git.getCommit(opts.config.settings.base_dir, req.params.repo, req.params.commit);
-
- reply.send({ data: commit });
- }
- });
-
- fastify_repo.route({
- method: "GET",
- path: "/tree",
- handler: async (req, reply) =>
- {
- const tree_path = (req.query.length !== 0 && req.query.path) ? req.query.path : null;
-
- const tree = await git.getTree(opts.config.settings.base_dir, req.params.repo, tree_path);
-
- if(tree.error) {
- if(tree.error === 404) {
- reply.code(404).send({ error: "Path not found" });
- }
- else {
- reply.code(500).send({ error: "Internal server error" });
- }
- }
- reply.send({ data: tree });
- }
- });
-
- done_repo();
- }, { prefix: "/repos/:repo" });
-
- done();
-}; \ No newline at end of file
diff --git a/packages/server/src/api/v1.ts b/packages/server/src/api/v1.ts
new file mode 100644
index 0000000..617e5f1
--- /dev/null
+++ b/packages/server/src/api/v1.ts
@@ -0,0 +1,123 @@
+import { verifyCommitID, verifyRepoName } from "./util";
+import { FastifyInstance } from "fastify";
+import { Git } from "./git";
+/* eslint-disable max-lines-per-function */
+
+export default function(fastify: FastifyInstance, opts, done) {
+ const git = new Git(opts.config.settings.base_dir);
+
+ fastify.route({
+ method: "GET",
+ url: "/info",
+ handler: (req, reply) => {
+ reply.send({ data: opts.config.settings });
+ }
+ });
+ fastify.route({
+ method: "GET",
+ url: "/repos",
+ handler: async(req, reply) => {
+ let repos = await git.getRepos();
+
+ if(repos["error"]) {
+ reply.code(500).send({ error: "Internal server error!" });
+ return;
+ }
+
+ reply.send({ data: repos });
+ }
+ });
+
+ fastify.route({
+ method: "GET",
+ url: "/repos/:repo",
+ handler: async(req, reply) => {
+ const params: any = req.params;
+ const repo_verification = await verifyRepoName(opts.config.settings.base_dir, params.repo);
+ if(repo_verification.success === false) {
+ reply.code(repo_verification.code).send(repo_verification.message);
+ }
+
+ const desc = await git.getRepoFile(params.repo, "description");
+
+ reply.send({ data: { name: params.repo, description: desc } });
+ }
+ });
+
+ fastify.register((fastify_repo, opts_repo, done_repo) => {
+ fastify_repo.addHook("onRequest", async(req, reply) => {
+ const params: any = req.params;
+ const repo_verification = await verifyRepoName(opts.config.settings.base_dir, params.repo);
+ if(repo_verification.success === false) {
+ reply.code(repo_verification.code).send({ error: repo_verification.message });
+ }
+ });
+
+ fastify_repo.route({
+ method: "GET",
+ url: "/log",
+ handler: async(req, reply) => {
+ const log = await git.getLog((<any>req.params).repo);
+
+ if(log["error"]) {
+ if(typeof log["error"] === "string") {
+ reply.code(500).send({ error: log["error"] });
+ }
+
+ switch(log["error"]) {
+ case 404:
+ reply.code(404).send({ error: "Git repository not found!" });
+ return;
+ default:
+ reply.code(500).send({ error: "Internal server error!" });
+ return;
+ }
+ }
+ reply.send({ data: log });
+ }
+ });
+
+ fastify_repo.route({
+ method: "GET",
+ url: "/log/:commit",
+ handler: async(req, reply) => {
+ const params: any = req.params;
+ const commit_verification = await verifyCommitID(git, params.repo, params.commit);
+ if(commit_verification.success === false) {
+ reply.code(commit_verification.code).send(commit_verification.message);
+ }
+
+ const commit = await git.getCommit(params.repo, params.commit);
+
+ reply.send({ data: commit });
+ }
+ });
+
+ fastify_repo.route({
+ method: "GET",
+ url: "/tree",
+ handler: async(req, reply) => {
+ const params: any = req.params;
+ const query: any = req.query;
+
+ const tree_path = (query.length !== 0 && query.path) ? query.path : null;
+
+ const tree = await git.getTree(params.repo, tree_path);
+
+ if(tree.error) {
+ if(tree.error === 404) {
+ reply.code(404).send({ error: "Path not found" });
+ }
+ else {
+ reply.code(500).send({ error: "Internal server error" });
+ }
+ }
+ reply.send({ data: tree });
+ }
+ });
+
+ done_repo();
+ }, { prefix: "/repos/:repo" });
+
+ done();
+}; \ No newline at end of file
diff --git a/packages/server/src/app.js b/packages/server/src/app.js
deleted file mode 100644
index a106f7d..0000000
--- a/packages/server/src/app.js
+++ /dev/null
@@ -1,98 +0,0 @@
-const fastify = require("fastify")();
-const api = require("./api/v1");
-const yaml = require("js-yaml");
-const fs = require("fs");
-const { exit } = require("process");
-const git = require("./api/git");
-const path = require("path");
-
-const settings = yaml.load(fs.readFileSync(__dirname + "/../../../settings.yml", "utf8"));
-const settings_keys = Object.keys(settings);
-
-const mandatory_settings = [ "host", "port", "dev_port", "title", "about", "base_dir", "production" ];
-
-// Make sure that all the required settings are present
-const settings_not_included = mandatory_settings.filter(x => !settings_keys.includes(x));
-if(settings_not_included.length !== 0) {
- console.log(`Error: settings.yml is missing ${(settings_not_included.length > 1) ? "keys" : "key"}:`);
- console.log(settings_not_included.join(", "));
- exit(1);
-}
-
-// Make sure that there's not an excessive amount of settings
-const mandatory_not_included = settings_keys.filter(x => !mandatory_settings.includes(x));
-if(mandatory_not_included.length !== 0) {
- console.log(`Error: settings.yml includes ${(mandatory_not_included.length > 1) ? "pointless keys" : "a pointless key"}:`);
- console.log(mandatory_not_included.join(", "));
- exit(1);
-}
-
-// Make sure that the base directory specified in the settings actually exists
-try {
- fs.readdirSync(settings["base_dir"]);
-}
-catch {
- console.error(`Error: Tried opening the base directory. No such directory: ${settings["base_dir"]}`);
- exit(1);
-}
-
-fastify.setNotFoundHandler({
- preValidation: (req, reply, done) => done(),
- preHandler: (req, reply, done) => done()
-}, function (req, reply)
-{
- reply.send("404: Not found");
-});
-
-if(settings.production) {
- fastify.register(require("fastify-static"), { root: path.join(__dirname, "/../../../dist") });
-
- fastify.route({
- method: "GET",
- path: "/",
- handler: (req, reply) => reply.sendFile("index.html")
- });
-}
-
-fastify.addContentTypeParser("application/x-git-upload-pack-request", (req, payload, done) => done(null, payload));
-
-fastify.register(api, { prefix: "/api/v1", config: { settings: settings } });
-
-fastify.route({
- method: "GET",
- path: "/:repo([a-zA-Z0-9\\.\\-_]+)/info/refs",
- handler: (req, reply) =>
- {
- if(!req.query.service) {
- reply.code(403).send("Missing service query parameter\n");
- return;
- }
- else if(req.query.service !== "git-upload-pack") {
- reply.code(403).send("Access denied!\n");
- return;
- }
- else if(Object.keys(req.query).length !== 1) {
- reply.header("Content-Type", "application/x-git-upload-pack-advertisement");
- reply.code(403).send("Too many query parameters!\n");
- return;
- }
-
- git.connectToGitHTTPBackend(settings["base_dir"], req, reply);
- }
-});
-
-fastify.route({
- method: "POST",
- path: "/:repo([a-zA-Z0-9\\.\\-_]+)/git-upload-pack",
- handler: (req, reply) => git.connectToGitHTTPBackend(settings["base_dir"], req, reply)
-});
-
-fastify.listen(settings.port, settings.host, (err, addr) =>
-{
- if(err) {
- console.error(err);
- exit(1);
- }
-
- console.log(`App is running on ${addr}`);
-}); \ No newline at end of file
diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts
new file mode 100644
index 0000000..810ca65
--- /dev/null
+++ b/packages/server/src/app.ts
@@ -0,0 +1,127 @@
+import { readFileSync, readdirSync } from "fs";
+import { Git } from "./api/git";
+import api from "./api/v1";
+import { exit } from "process";
+import { fastify as fastifyFactory } from "fastify";
+import { join } from "path";
+import { load } from "js-yaml";
+import { verifyRepoName } from "./api/util";
+
+const settings = <any>load(readFileSync(join(__dirname, "/../../../settings.yml"), "utf8"));
+const settings_keys = Object.keys(settings);
+
+const mandatory_settings = [ "host", "port", "dev_port", "title", "about", "base_dir", "production" ];
+
+// Make sure that all the required settings are present
+const settings_not_included = mandatory_settings.filter(x => !settings_keys.includes(x));
+if(settings_not_included.length !== 0) {
+ console.log(`Error: settings.yml is missing ${(settings_not_included.length > 1) ? "keys" : "key"}:`);
+ console.log(settings_not_included.join(", "));
+ exit(1);
+}
+
+// Make sure that there's not an excessive amount of settings
+const mandatory_not_included = settings_keys.filter(x => !mandatory_settings.includes(x));
+if(mandatory_not_included.length !== 0) {
+ console.log(`Error: settings.yml includes ${(mandatory_not_included.length > 1) ? "pointless keys" : "a pointless key"}:`);
+ console.log(mandatory_not_included.join(", "));
+ exit(1);
+}
+
+// Make sure that the base directory specified in the settings actually exists
+try {
+ readdirSync(settings.base_dir);
+}
+catch {
+ console.error(`Error: Tried opening the base directory. No such directory: ${settings.base_dir}`);
+ exit(1);
+}
+
+const dist_dir = join(__dirname, "/../../client/dist");
+
+if(settings.production) {
+ try {
+ readdirSync(dist_dir);
+ }
+ catch {
+ console.error("Error: Tried opening the dist directory but it doesn't exist.\nDid you accidentally turn on the production setting?");
+ exit(1);
+ }
+}
+
+const fastify = fastifyFactory();
+const git = new Git(settings.base_dir);
+
+fastify.setNotFoundHandler({}, function(req, reply) {
+ reply.code(404).send("Page not found!");
+});
+
+if(settings.production) {
+ fastify.register(require("fastify-static"), { root: dist_dir });
+
+ fastify.route({
+ method: "GET",
+ url: "/",
+ handler: (req, reply: any) => reply.sendFile("index.html")
+ });
+}
+
+fastify.addContentTypeParser("application/x-git-upload-pack-request", (req, payload, done) => done(null, payload));
+
+fastify.register(api, { prefix: "/api/v1", config: { settings: settings } });
+
+interface Query {
+ [key: string]: string
+}
+
+fastify.route({
+ method: "GET",
+ url: "/:repo([a-zA-Z0-9\\.\\-_]+)/info/refs",
+ handler: async(req, reply) => {
+ reply.header("Content-Type", "application/x-git-upload-pack-advertisement");
+
+ const repo_verification = await verifyRepoName(settings.base_dir, (<any>req).params.repo);
+ if(repo_verification.success === false) {
+ reply.code(repo_verification.code).send(repo_verification.message);
+ }
+
+ const query: Query = <any>req.query;
+ if(!query.service) {
+ reply.code(403).send("Missing service query parameter\n");
+ return;
+ }
+ else if(query.service !== "git-upload-pack") {
+ reply.code(403).send("Access denied!\n");
+ return;
+ }
+ else if(Object.keys(query).length !== 1) {
+ reply.code(403).send("Too many query parameters!\n");
+ return;
+ }
+
+ git.connectToGitHTTPBackend(req, reply);
+ }
+});
+
+fastify.route({
+ method: "POST",
+ url: "/:repo([a-zA-Z0-9\\.\\-_]+)/git-upload-pack",
+ handler: async(req, reply) => {
+ const repo_verification = await verifyRepoName(settings.base_dir, (<any>req).params.repo);
+ if(repo_verification.success === false) {
+ reply.header("Content-Type", "application/x-git-upload-pack-result");
+ reply.code(repo_verification.code).send(repo_verification.message);
+ }
+
+ git.connectToGitHTTPBackend(req, reply);
+ }
+});
+
+fastify.listen(settings.port, settings.host, (err: Error, addr: string) => {
+ if(err) {
+ console.error(err);
+ exit(1);
+ }
+
+ console.log(`App is running on ${addr}`);
+}); \ No newline at end of file
diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json
new file mode 100644
index 0000000..5dfdda7
--- /dev/null
+++ b/packages/server/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "module": "commonjs",
+ "outDir": "dist",
+ "strict": false,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ }
+}