diff options
-rw-r--r-- | packages/server/.eslintrc.js | 559 | ||||
-rw-r--r-- | packages/server/src/api/git.js | 412 | ||||
-rw-r--r-- | packages/server/src/api/git.ts | 395 | ||||
-rw-r--r-- | packages/server/src/api/util.js | 45 | ||||
-rw-r--r-- | packages/server/src/api/util.ts | 79 | ||||
-rw-r--r-- | packages/server/src/api/v1.js | 136 | ||||
-rw-r--r-- | packages/server/src/api/v1.ts | 123 | ||||
-rw-r--r-- | packages/server/src/app.js | 98 | ||||
-rw-r--r-- | packages/server/src/app.ts | 127 | ||||
-rw-r--r-- | packages/server/tsconfig.json | 11 |
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 + } +} |