Add tree route and recursive option (#52)

* Add tree route and recursive option

* Add recursive to swagger

* Fix lint

* Address feedback

* Fix lint

* Bump package

* Rename variable

* Fix missing path in spec

* Fix logic

* Add content test

* Add test for tree controller

* Update branches snap

* Fix snap

* Small fixes

* Fix lint
This commit is contained in:
billytrend 2019-07-23 12:47:33 -07:00 коммит произвёл GitHub
Родитель 638ea5b524
Коммит c4a58efd9d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 770 добавлений и 49 удалений

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "git-rest-api",
"version": "0.3.2",
"version": "0.3.3",
"description": "This project welcomes contributions and suggestions. Most contributions require you to agree to a\r Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\r the rights to use your contribution. For details, visit https://cla.microsoft.com.",
"main": "bin/main.js",
"scripts": {

Просмотреть файл

@ -10,6 +10,7 @@ import {
ContentController,
HealthCheckController,
} from "./controllers";
import { TreeController } from "./controllers/tree/tree.controller";
import { Telemetry, createTelemetry } from "./core";
import { ContextMiddleware, LoggingInterceptor } from "./middlewares";
import {
@ -31,7 +32,14 @@ import { RepoIndexService } from "./services/repo-index";
@Module({
imports: [],
controllers: [HealthCheckController, BranchesController, CommitsController, CompareController, ContentController],
controllers: [
HealthCheckController,
BranchesController,
CommitsController,
CompareController,
ContentController,
TreeController,
],
providers: [
AppService,
CompareService,

Просмотреть файл

@ -2,7 +2,7 @@ import { Controller, Get, HttpException, Param } from "@nestjs/common";
import { ApiNotFoundResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
import { ApiHasPassThruAuth, Auth, RepoAuth } from "../../core";
import { GitDiff } from "../../dtos/git-diff";
import { GitDiff } from "../../dtos";
import { CompareService } from "../../services";
@Controller("/repos/:remote/compare")

Просмотреть файл

@ -0,0 +1,237 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/contents' 1`] = `
Object {
"dirs": Array [
Object {
"name": "dir1",
"path": "dir1",
"sha": "b638a8a4a9f44184a3a430988a9c5ef383bad364",
"size": 0,
"type": "dir",
},
],
"files": Array [
Object {
"content": "IyBMb2dzCmxvZ3MKKi5sb2cKbnBtLWRlYnVnLmxvZyoKeWFybi1kZWJ1Zy5sb2cqCnlhcm4tZXJyb3IubG9nKgoKIyBSdW50aW1lIGRhdGEKcGlkcwoqLnBpZAoqLnNlZWQKKi5waWQubG9jawoKIyBEaXJlY3RvcnkgZm9yIGluc3RydW1lbnRlZCBsaWJzIGdlbmVyYXRlZCBieSBqc2NvdmVyYWdlL0pTQ292ZXIKbGliLWNvdgoKIyBDb3ZlcmFnZSBkaXJlY3RvcnkgdXNlZCBieSB0b29scyBsaWtlIGlzdGFuYnVsCmNvdmVyYWdlCgojIG55YyB0ZXN0IGNvdmVyYWdlCi5ueWNfb3V0cHV0CgojIEdydW50IGludGVybWVkaWF0ZSBzdG9yYWdlIChodHRwOi8vZ3J1bnRqcy5jb20vY3JlYXRpbmctcGx1Z2lucyNzdG9yaW5nLXRhc2stZmlsZXMpCi5ncnVudAoKIyBCb3dlciBkZXBlbmRlbmN5IGRpcmVjdG9yeSAoaHR0cHM6Ly9ib3dlci5pby8pCmJvd2VyX2NvbXBvbmVudHMKCiMgbm9kZS13YWYgY29uZmlndXJhdGlvbgoubG9jay13c2NyaXB0CgojIENvbXBpbGVkIGJpbmFyeSBhZGRvbnMgKGh0dHBzOi8vbm9kZWpzLm9yZy9hcGkvYWRkb25zLmh0bWwpCmJ1aWxkL1JlbGVhc2UKCiMgRGVwZW5kZW5jeSBkaXJlY3Rvcmllcwpub2RlX21vZHVsZXMvCmpzcG1fcGFja2FnZXMvCgojIFR5cGVTY3JpcHQgdjEgZGVjbGFyYXRpb24gZmlsZXMKdHlwaW5ncy8KCiMgT3B0aW9uYWwgbnBtIGNhY2hlIGRpcmVjdG9yeQoubnBtCgojIE9wdGlvbmFsIGVzbGludCBjYWNoZQouZXNsaW50Y2FjaGUKCiMgT3B0aW9uYWwgUkVQTCBoaXN0b3J5Ci5ub2RlX3JlcGxfaGlzdG9yeQoKIyBPdXRwdXQgb2YgJ25wbSBwYWNrJwoqLnRnegoKIyBZYXJuIEludGVncml0eSBmaWxlCi55YXJuLWludGVncml0eQoKIyBkb3RlbnYgZW52aXJvbm1lbnQgdmFyaWFibGVzIGZpbGUKLmVudgoKIyBuZXh0LmpzIGJ1aWxkIG91dHB1dAoubmV4dAo=",
"encoding": "base64",
"name": ".gitignore",
"path": ".gitignore",
"sha": "ad46b30886fa350c1f59761b100e5e4b01f9a7ec",
"size": 914,
"type": "file",
},
Object {
"content": "TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAxOSB0ZXN0LXJlcG8tYmlsbHkKClBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhIGNvcHkKb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKaW4gdGhlIFNvZnR3YXJlIHdpdGhvdXQgcmVzdHJpY3Rpb24sIGluY2x1ZGluZyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cwp0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsCmNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwpmdXJuaXNoZWQgdG8gZG8gc28sIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOgoKVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWQgaW4gYWxsCmNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgpJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCkFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKU09GVFdBUkUuCg==",
"encoding": "base64",
"name": "LICENSE",
"path": "LICENSE",
"sha": "98023418cdb98210a5f71ea74ec557dbbd8f0e83",
"size": 1072,
"type": "file",
},
Object {
"content": "IyBnaXQtYXBpLXRlc3RzClJlcG8gdXNlZCBmb3IgaW50ZWdyYXRpb24gdGVzdGluZyBvZiB0aGUgZ2l0LXRlc3QtYXBpIHByb2plY3QK",
"encoding": "base64",
"name": "README.md",
"path": "README.md",
"sha": "b5fd37e731f1e7931da42484ae0290554cb42c0f",
"size": 78,
"type": "file",
},
],
"submodules": Array [],
}
`;
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/contents/' 1`] = `
Object {
"dirs": Array [
Object {
"name": "dir1",
"path": "dir1",
"sha": "b638a8a4a9f44184a3a430988a9c5ef383bad364",
"size": 0,
"type": "dir",
},
],
"files": Array [
Object {
"content": "IyBMb2dzCmxvZ3MKKi5sb2cKbnBtLWRlYnVnLmxvZyoKeWFybi1kZWJ1Zy5sb2cqCnlhcm4tZXJyb3IubG9nKgoKIyBSdW50aW1lIGRhdGEKcGlkcwoqLnBpZAoqLnNlZWQKKi5waWQubG9jawoKIyBEaXJlY3RvcnkgZm9yIGluc3RydW1lbnRlZCBsaWJzIGdlbmVyYXRlZCBieSBqc2NvdmVyYWdlL0pTQ292ZXIKbGliLWNvdgoKIyBDb3ZlcmFnZSBkaXJlY3RvcnkgdXNlZCBieSB0b29scyBsaWtlIGlzdGFuYnVsCmNvdmVyYWdlCgojIG55YyB0ZXN0IGNvdmVyYWdlCi5ueWNfb3V0cHV0CgojIEdydW50IGludGVybWVkaWF0ZSBzdG9yYWdlIChodHRwOi8vZ3J1bnRqcy5jb20vY3JlYXRpbmctcGx1Z2lucyNzdG9yaW5nLXRhc2stZmlsZXMpCi5ncnVudAoKIyBCb3dlciBkZXBlbmRlbmN5IGRpcmVjdG9yeSAoaHR0cHM6Ly9ib3dlci5pby8pCmJvd2VyX2NvbXBvbmVudHMKCiMgbm9kZS13YWYgY29uZmlndXJhdGlvbgoubG9jay13c2NyaXB0CgojIENvbXBpbGVkIGJpbmFyeSBhZGRvbnMgKGh0dHBzOi8vbm9kZWpzLm9yZy9hcGkvYWRkb25zLmh0bWwpCmJ1aWxkL1JlbGVhc2UKCiMgRGVwZW5kZW5jeSBkaXJlY3Rvcmllcwpub2RlX21vZHVsZXMvCmpzcG1fcGFja2FnZXMvCgojIFR5cGVTY3JpcHQgdjEgZGVjbGFyYXRpb24gZmlsZXMKdHlwaW5ncy8KCiMgT3B0aW9uYWwgbnBtIGNhY2hlIGRpcmVjdG9yeQoubnBtCgojIE9wdGlvbmFsIGVzbGludCBjYWNoZQouZXNsaW50Y2FjaGUKCiMgT3B0aW9uYWwgUkVQTCBoaXN0b3J5Ci5ub2RlX3JlcGxfaGlzdG9yeQoKIyBPdXRwdXQgb2YgJ25wbSBwYWNrJwoqLnRnegoKIyBZYXJuIEludGVncml0eSBmaWxlCi55YXJuLWludGVncml0eQoKIyBkb3RlbnYgZW52aXJvbm1lbnQgdmFyaWFibGVzIGZpbGUKLmVudgoKIyBuZXh0LmpzIGJ1aWxkIG91dHB1dAoubmV4dAo=",
"encoding": "base64",
"name": ".gitignore",
"path": ".gitignore",
"sha": "ad46b30886fa350c1f59761b100e5e4b01f9a7ec",
"size": 914,
"type": "file",
},
Object {
"content": "TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAxOSB0ZXN0LXJlcG8tYmlsbHkKClBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhIGNvcHkKb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKaW4gdGhlIFNvZnR3YXJlIHdpdGhvdXQgcmVzdHJpY3Rpb24sIGluY2x1ZGluZyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cwp0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsCmNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwpmdXJuaXNoZWQgdG8gZG8gc28sIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOgoKVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWQgaW4gYWxsCmNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgpJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCkFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKU09GVFdBUkUuCg==",
"encoding": "base64",
"name": "LICENSE",
"path": "LICENSE",
"sha": "98023418cdb98210a5f71ea74ec557dbbd8f0e83",
"size": 1072,
"type": "file",
},
Object {
"content": "IyBnaXQtYXBpLXRlc3RzClJlcG8gdXNlZCBmb3IgaW50ZWdyYXRpb24gdGVzdGluZyBvZiB0aGUgZ2l0LXRlc3QtYXBpIHByb2plY3QK",
"encoding": "base64",
"name": "README.md",
"path": "README.md",
"sha": "b5fd37e731f1e7931da42484ae0290554cb42c0f",
"size": 78,
"type": "file",
},
],
"submodules": Array [],
}
`;
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/contents/README.md' 1`] = `
Object {
"dirs": Array [],
"files": Array [
Object {
"content": "IyBnaXQtYXBpLXRlc3RzClJlcG8gdXNlZCBmb3IgaW50ZWdyYXRpb24gdGVzdGluZyBvZiB0aGUgZ2l0LXRlc3QtYXBpIHByb2plY3QK",
"encoding": "base64",
"name": "README.md",
"path": "README.md",
"sha": "b5fd37e731f1e7931da42484ae0290554cb42c0f",
"size": 78,
"type": "file",
},
],
"submodules": Array [],
}
`;
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/contents/dir1' 1`] = `
Object {
"dirs": Array [
Object {
"name": "dir2",
"path": "dir1/dir2",
"sha": "483221c9d8371862bdb2c5d452130ab5ca0534a3",
"size": 0,
"type": "dir",
},
],
"files": Array [
Object {
"content": "ZmlsZUEK",
"encoding": "base64",
"name": "fileA.txt",
"path": "dir1/fileA.txt",
"sha": "ab47708c98ac88bbdf3ca75f4730d86a84f702a2",
"size": 6,
"type": "file",
},
],
"submodules": Array [],
}
`;
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/contents/dir1?recursive=true' 1`] = `
Object {
"dirs": Array [
Object {
"name": "dir2",
"path": "dir1/dir2",
"sha": "483221c9d8371862bdb2c5d452130ab5ca0534a3",
"size": 0,
"type": "dir",
},
],
"files": Array [
Object {
"content": "ZmlsZUEK",
"encoding": "base64",
"name": "fileA.txt",
"path": "dir1/fileA.txt",
"sha": "ab47708c98ac88bbdf3ca75f4730d86a84f702a2",
"size": 6,
"type": "file",
},
Object {
"content": "ZmlsZUIK",
"encoding": "base64",
"name": "fileB.txt",
"path": "dir1/dir2/fileB.txt",
"sha": "78ed112c991c8abeba325c039a398ba626c425ab",
"size": 6,
"type": "file",
},
],
"submodules": Array [],
}
`;
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/contents?recursive=true' 1`] = `
Object {
"dirs": Array [
Object {
"name": "dir1",
"path": "dir1",
"sha": "b638a8a4a9f44184a3a430988a9c5ef383bad364",
"size": 0,
"type": "dir",
},
Object {
"name": "dir2",
"path": "dir1/dir2",
"sha": "483221c9d8371862bdb2c5d452130ab5ca0534a3",
"size": 0,
"type": "dir",
},
],
"files": Array [
Object {
"content": "IyBMb2dzCmxvZ3MKKi5sb2cKbnBtLWRlYnVnLmxvZyoKeWFybi1kZWJ1Zy5sb2cqCnlhcm4tZXJyb3IubG9nKgoKIyBSdW50aW1lIGRhdGEKcGlkcwoqLnBpZAoqLnNlZWQKKi5waWQubG9jawoKIyBEaXJlY3RvcnkgZm9yIGluc3RydW1lbnRlZCBsaWJzIGdlbmVyYXRlZCBieSBqc2NvdmVyYWdlL0pTQ292ZXIKbGliLWNvdgoKIyBDb3ZlcmFnZSBkaXJlY3RvcnkgdXNlZCBieSB0b29scyBsaWtlIGlzdGFuYnVsCmNvdmVyYWdlCgojIG55YyB0ZXN0IGNvdmVyYWdlCi5ueWNfb3V0cHV0CgojIEdydW50IGludGVybWVkaWF0ZSBzdG9yYWdlIChodHRwOi8vZ3J1bnRqcy5jb20vY3JlYXRpbmctcGx1Z2lucyNzdG9yaW5nLXRhc2stZmlsZXMpCi5ncnVudAoKIyBCb3dlciBkZXBlbmRlbmN5IGRpcmVjdG9yeSAoaHR0cHM6Ly9ib3dlci5pby8pCmJvd2VyX2NvbXBvbmVudHMKCiMgbm9kZS13YWYgY29uZmlndXJhdGlvbgoubG9jay13c2NyaXB0CgojIENvbXBpbGVkIGJpbmFyeSBhZGRvbnMgKGh0dHBzOi8vbm9kZWpzLm9yZy9hcGkvYWRkb25zLmh0bWwpCmJ1aWxkL1JlbGVhc2UKCiMgRGVwZW5kZW5jeSBkaXJlY3Rvcmllcwpub2RlX21vZHVsZXMvCmpzcG1fcGFja2FnZXMvCgojIFR5cGVTY3JpcHQgdjEgZGVjbGFyYXRpb24gZmlsZXMKdHlwaW5ncy8KCiMgT3B0aW9uYWwgbnBtIGNhY2hlIGRpcmVjdG9yeQoubnBtCgojIE9wdGlvbmFsIGVzbGludCBjYWNoZQouZXNsaW50Y2FjaGUKCiMgT3B0aW9uYWwgUkVQTCBoaXN0b3J5Ci5ub2RlX3JlcGxfaGlzdG9yeQoKIyBPdXRwdXQgb2YgJ25wbSBwYWNrJwoqLnRnegoKIyBZYXJuIEludGVncml0eSBmaWxlCi55YXJuLWludGVncml0eQoKIyBkb3RlbnYgZW52aXJvbm1lbnQgdmFyaWFibGVzIGZpbGUKLmVudgoKIyBuZXh0LmpzIGJ1aWxkIG91dHB1dAoubmV4dAo=",
"encoding": "base64",
"name": ".gitignore",
"path": ".gitignore",
"sha": "ad46b30886fa350c1f59761b100e5e4b01f9a7ec",
"size": 914,
"type": "file",
},
Object {
"content": "TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAxOSB0ZXN0LXJlcG8tYmlsbHkKClBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhIGNvcHkKb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKaW4gdGhlIFNvZnR3YXJlIHdpdGhvdXQgcmVzdHJpY3Rpb24sIGluY2x1ZGluZyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cwp0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsCmNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwpmdXJuaXNoZWQgdG8gZG8gc28sIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOgoKVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWQgaW4gYWxsCmNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgpJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCkFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKU09GVFdBUkUuCg==",
"encoding": "base64",
"name": "LICENSE",
"path": "LICENSE",
"sha": "98023418cdb98210a5f71ea74ec557dbbd8f0e83",
"size": 1072,
"type": "file",
},
Object {
"content": "IyBnaXQtYXBpLXRlc3RzClJlcG8gdXNlZCBmb3IgaW50ZWdyYXRpb24gdGVzdGluZyBvZiB0aGUgZ2l0LXRlc3QtYXBpIHByb2plY3QK",
"encoding": "base64",
"name": "README.md",
"path": "README.md",
"sha": "b5fd37e731f1e7931da42484ae0290554cb42c0f",
"size": 78,
"type": "file",
},
Object {
"content": "ZmlsZUEK",
"encoding": "base64",
"name": "fileA.txt",
"path": "dir1/fileA.txt",
"sha": "ab47708c98ac88bbdf3ca75f4730d86a84f702a2",
"size": 6,
"type": "file",
},
Object {
"content": "ZmlsZUIK",
"encoding": "base64",
"name": "fileB.txt",
"path": "dir1/dir2/fileB.txt",
"sha": "78ed112c991c8abeba325c039a398ba626c425ab",
"size": 6,
"type": "file",
},
],
"submodules": Array [],
}
`;

Просмотреть файл

@ -0,0 +1,15 @@
import { TEST_REPO, e2eClient } from "../../../test/e2e";
describe("Test content controller", () => {
const base = `/repos/${TEST_REPO}/contents`;
test.each(["", "/", "/README.md", "/dir1", "/dir1?recursive=true", "?recursive=true"])(
`for path '${base}%s'`,
async tail => {
const response = await e2eClient.fetch(`${base}${tail}`);
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toMatchSnapshot();
},
);
});

Просмотреть файл

@ -1,27 +1,40 @@
import { Controller, Get, HttpException, Param, Query } from "@nestjs/common";
import { ApiImplicitQuery, ApiNotFoundResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
import { ApiImplicitParam, ApiImplicitQuery, ApiNotFoundResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
import { ApiHasPassThruAuth, Auth, RepoAuth } from "../../core";
import { GitContents } from "../../dtos/git-contents";
import { GitContents } from "../../dtos";
import { ContentService } from "../../services/content";
import { parseBooleanFromURLParam } from "../../utils";
@Controller("/repos/:remote/contents")
export class ContentController {
constructor(private contentService: ContentService) {}
@Get([":path([^/]*)", "*"])
@Get([":path([^/]*)", ""])
@ApiHasPassThruAuth()
@ApiOkResponse({ type: GitContents })
@ApiImplicitQuery({ name: "ref", required: false, type: "string" })
@ApiImplicitQuery({ name: "recursive", required: false, type: "string" })
@ApiImplicitParam({ name: "path", type: "string" })
@ApiOperation({ title: "Get content", operationId: "contents_get" })
@ApiNotFoundResponse({})
public async getContents(
@Param("remote") remote: string,
@Param("path") path: string | undefined,
@Query("ref") ref: string | undefined,
@Query("recursive") recursive: string | undefined,
@Auth() auth: RepoAuth,
) {
const content = await this.contentService.getContents(remote, path, ref, { auth });
const content = await this.contentService.getContents(
remote,
path,
ref,
parseBooleanFromURLParam(recursive),
true,
{
auth,
},
);
if (content instanceof HttpException) {
throw content;
}

Просмотреть файл

@ -0,0 +1,179 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/tree' 1`] = `
Object {
"dirs": Array [
Object {
"name": "dir1",
"path": "dir1",
"sha": "b638a8a4a9f44184a3a430988a9c5ef383bad364",
"size": 0,
"type": "dir",
},
Object {
"name": "dir2",
"path": "dir1/dir2",
"sha": "483221c9d8371862bdb2c5d452130ab5ca0534a3",
"size": 0,
"type": "dir",
},
],
"files": Array [
Object {
"encoding": "base64",
"name": ".gitignore",
"path": ".gitignore",
"sha": "ad46b30886fa350c1f59761b100e5e4b01f9a7ec",
"size": 914,
"type": "file",
},
Object {
"encoding": "base64",
"name": "LICENSE",
"path": "LICENSE",
"sha": "98023418cdb98210a5f71ea74ec557dbbd8f0e83",
"size": 1072,
"type": "file",
},
Object {
"encoding": "base64",
"name": "README.md",
"path": "README.md",
"sha": "b5fd37e731f1e7931da42484ae0290554cb42c0f",
"size": 78,
"type": "file",
},
Object {
"encoding": "base64",
"name": "fileA.txt",
"path": "dir1/fileA.txt",
"sha": "ab47708c98ac88bbdf3ca75f4730d86a84f702a2",
"size": 6,
"type": "file",
},
Object {
"encoding": "base64",
"name": "fileB.txt",
"path": "dir1/dir2/fileB.txt",
"sha": "78ed112c991c8abeba325c039a398ba626c425ab",
"size": 6,
"type": "file",
},
],
"submodules": Array [],
}
`;
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/tree/' 1`] = `
Object {
"dirs": Array [
Object {
"name": "dir1",
"path": "dir1",
"sha": "b638a8a4a9f44184a3a430988a9c5ef383bad364",
"size": 0,
"type": "dir",
},
Object {
"name": "dir2",
"path": "dir1/dir2",
"sha": "483221c9d8371862bdb2c5d452130ab5ca0534a3",
"size": 0,
"type": "dir",
},
],
"files": Array [
Object {
"encoding": "base64",
"name": ".gitignore",
"path": ".gitignore",
"sha": "ad46b30886fa350c1f59761b100e5e4b01f9a7ec",
"size": 914,
"type": "file",
},
Object {
"encoding": "base64",
"name": "LICENSE",
"path": "LICENSE",
"sha": "98023418cdb98210a5f71ea74ec557dbbd8f0e83",
"size": 1072,
"type": "file",
},
Object {
"encoding": "base64",
"name": "README.md",
"path": "README.md",
"sha": "b5fd37e731f1e7931da42484ae0290554cb42c0f",
"size": 78,
"type": "file",
},
Object {
"encoding": "base64",
"name": "fileA.txt",
"path": "dir1/fileA.txt",
"sha": "ab47708c98ac88bbdf3ca75f4730d86a84f702a2",
"size": 6,
"type": "file",
},
Object {
"encoding": "base64",
"name": "fileB.txt",
"path": "dir1/dir2/fileB.txt",
"sha": "78ed112c991c8abeba325c039a398ba626c425ab",
"size": 6,
"type": "file",
},
],
"submodules": Array [],
}
`;
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/tree/README.md' 1`] = `
Object {
"dirs": Array [],
"files": Array [
Object {
"encoding": "base64",
"name": "README.md",
"path": "README.md",
"sha": "b5fd37e731f1e7931da42484ae0290554cb42c0f",
"size": 78,
"type": "file",
},
],
"submodules": Array [],
}
`;
exports[`Test content controller for path '/repos/github.com%2Ftest-repo-billy%2Fgit-api-tests/tree/dir1' 1`] = `
Object {
"dirs": Array [
Object {
"name": "dir2",
"path": "dir1/dir2",
"sha": "483221c9d8371862bdb2c5d452130ab5ca0534a3",
"size": 0,
"type": "dir",
},
],
"files": Array [
Object {
"encoding": "base64",
"name": "fileA.txt",
"path": "dir1/fileA.txt",
"sha": "ab47708c98ac88bbdf3ca75f4730d86a84f702a2",
"size": 6,
"type": "file",
},
Object {
"encoding": "base64",
"name": "fileB.txt",
"path": "dir1/dir2/fileB.txt",
"sha": "78ed112c991c8abeba325c039a398ba626c425ab",
"size": 6,
"type": "file",
},
],
"submodules": Array [],
}
`;

Просмотреть файл

@ -0,0 +1,12 @@
import { TEST_REPO, e2eClient } from "../../../test/e2e";
describe("Test content controller", () => {
const base = `/repos/${TEST_REPO}/tree`;
test.each(["", "/", "/README.md", "/dir1"])(`for path '${base}%s'`, async tail => {
const response = await e2eClient.fetch(`${base}${tail}`);
expect(response.status).toEqual(200);
const body = await response.json();
expect(body).toMatchSnapshot();
});
});

Просмотреть файл

@ -0,0 +1,31 @@
import { Controller, Get, HttpException, Param, Query } from "@nestjs/common";
import { ApiImplicitParam, ApiImplicitQuery, ApiNotFoundResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
import { ApiHasPassThruAuth, Auth, RepoAuth } from "../../core";
import { GitTree } from "../../dtos";
import { ContentService } from "../../services/content";
@Controller("/repos/:remote/tree")
export class TreeController {
constructor(private contentService: ContentService) {}
@Get([":path([^/]*)", ""])
@ApiHasPassThruAuth()
@ApiOkResponse({ type: GitTree })
@ApiImplicitQuery({ name: "ref", required: false, type: "string" })
@ApiOperation({ title: "Get tree", operationId: "tree_get" })
@ApiImplicitParam({ name: "path", type: "string" })
@ApiNotFoundResponse({})
public async getTree(
@Param("remote") remote: string,
@Param("path") path: string | undefined,
@Query("ref") ref: string | undefined,
@Auth() auth: RepoAuth,
) {
const tree = await this.contentService.getContents(remote, path, ref, true, false, { auth });
if (tree instanceof HttpException) {
throw tree;
}
return tree;
}
}

Просмотреть файл

@ -1,18 +1,18 @@
import { ApiModelProperty } from "@nestjs/swagger";
import { GitDirObjectContent } from "./git-dir-object-content";
import { GitFileObjectContent } from "./git-file-object-content";
import { GitFileObjectWithContent } from "./git-file-object-with-content";
import { GitSubmoduleObjectContent } from "./git-submodule-object-content";
export class GitContents {
export class GitTree {
@ApiModelProperty({ type: GitDirObjectContent, isArray: true })
public dirs: GitDirObjectContent[];
@ApiModelProperty({ type: GitFileObjectContent, isArray: true })
public files: GitFileObjectContent[];
@ApiModelProperty({ type: GitFileObjectWithContent, isArray: true })
public files: GitFileObjectWithContent[];
@ApiModelProperty({ type: GitSubmoduleObjectContent, isArray: true })
public submodules: GitSubmoduleObjectContent[];
constructor(gitObjectContent: GitContents) {
constructor(gitObjectContent: GitTree) {
this.dirs = gitObjectContent.dirs;
this.files = gitObjectContent.files;
this.submodules = gitObjectContent.submodules;

Просмотреть файл

@ -0,0 +1,13 @@
import { ApiModelProperty } from "@nestjs/swagger";
import { GitFileObjectWithoutContent } from "./git-file-object-without-content";
export class GitFileObjectWithContent extends GitFileObjectWithoutContent {
@ApiModelProperty({ type: String })
public content: string;
constructor(gitObjectContent: GitFileObjectWithContent) {
super(gitObjectContent);
this.content = gitObjectContent.content;
}
}

Просмотреть файл

@ -2,15 +2,12 @@ import { ApiModelProperty } from "@nestjs/swagger";
import { GitObjectContent } from "./git-object-content";
export class GitFileObjectContent extends GitObjectContent {
@ApiModelProperty({ type: String })
public content: string;
export class GitFileObjectWithoutContent extends GitObjectContent {
@ApiModelProperty({ type: String })
public encoding: string;
constructor(gitObjectContent: GitFileObjectContent) {
constructor(gitObjectContent: GitFileObjectWithoutContent) {
super(gitObjectContent);
this.content = gitObjectContent.content;
this.encoding = gitObjectContent.encoding;
}
}

20
src/dtos/git-tree.ts Normal file
Просмотреть файл

@ -0,0 +1,20 @@
import { ApiModelProperty } from "@nestjs/swagger";
import { GitDirObjectContent } from "./git-dir-object-content";
import { GitFileObjectWithoutContent } from "./git-file-object-without-content";
import { GitSubmoduleObjectContent } from "./git-submodule-object-content";
export class GitContents {
@ApiModelProperty({ type: GitDirObjectContent, isArray: true })
public dirs: GitDirObjectContent[];
@ApiModelProperty({ type: GitFileObjectWithoutContent, isArray: true })
public files: GitFileObjectWithoutContent[];
@ApiModelProperty({ type: GitSubmoduleObjectContent, isArray: true })
public submodules: GitSubmoduleObjectContent[];
constructor(gitObjectContent: GitContents) {
this.dirs = gitObjectContent.dirs;
this.files = gitObjectContent.files;
this.submodules = gitObjectContent.submodules;
}
}

Просмотреть файл

@ -1,5 +1,13 @@
export * from "./git-branch";
export * from "./git-commit";
export * from "./git-commit-ref";
export * from "./git-signature";
export * from "./git-commit";
export * from "./git-contents";
export * from "./git-diff";
export * from "./git-dir-object-content";
export * from "./git-file-diff";
export * from "./git-file-object-with-content";
export * from "./git-file-object-without-content";
export * from "./git-object-content";
export * from "./git-signature";
export * from "./git-submodule-object-content";
export * from "./git-tree";

Просмотреть файл

@ -2,8 +2,7 @@ import { Injectable, NotFoundException } from "@nestjs/common";
import { Commit, Oid, Repository, Revwalk, Signature, Time } from "nodegit";
import { PaginatedList, Pagination, getPage, getPaginationSkip } from "../../core";
import { GitCommit, GitCommitRef } from "../../dtos";
import { GitSignature } from "../../dtos/git-signature";
import { GitCommit, GitCommitRef, GitSignature } from "../../dtos";
import { GitBaseOptions, RepoService } from "../repo";
const LIST_COMMIT_PAGE_SIZE = 100;

Просмотреть файл

@ -2,8 +2,7 @@ import { Injectable, NotFoundException } from "@nestjs/common";
import { Commit, ConvenientPatch, Diff, Merge, Oid, Repository } from "nodegit";
import { Logger } from "../../core";
import { GitFileDiff, PatchStatus } from "../../dtos";
import { GitDiff } from "../../dtos/git-diff";
import { GitDiff, GitFileDiff, PatchStatus } from "../../dtos";
import { GitUtils, notUndefined } from "../../utils";
import { CommitService, toGitCommit } from "../commit";
import { GitBaseOptions, RepoService } from "../repo";

Просмотреть файл

@ -1,10 +1,14 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { Repository, TreeEntry } from "nodegit";
import { Repository, Tree, TreeEntry } from "nodegit";
import { GitContents } from "../../dtos/git-contents";
import { GitDirObjectContent } from "../../dtos/git-dir-object-content";
import { GitFileObjectContent } from "../../dtos/git-file-object-content";
import { GitSubmoduleObjectContent } from "../../dtos/git-submodule-object-content";
import {
GitContents,
GitDirObjectContent,
GitFileObjectWithContent,
GitFileObjectWithoutContent,
GitSubmoduleObjectContent,
GitTree,
} from "../../dtos";
import { CommitService } from "../commit";
import { GitBaseOptions, RepoService } from "../repo";
@ -16,14 +20,22 @@ export class ContentService {
remote: string,
path: string | undefined,
ref: string | undefined = "master",
recursive: boolean = false,
includeContents: boolean = true,
options: GitBaseOptions = {},
): Promise<GitContents | NotFoundException> {
): Promise<GitContents | GitTree | NotFoundException> {
return this.repoService.use(remote, options, async repo => {
return this.getGitContents(repo, path, ref);
return this.getGitContents(repo, path, recursive, includeContents, ref);
});
}
public async getGitContents(repo: Repository, path: string | undefined, ref: string | undefined = "master") {
public async getGitContents(
repo: Repository,
path: string | undefined,
recursive: boolean,
includeContents: boolean,
ref: string | undefined = "master",
) {
const commit = await this.commitService.getCommit(repo, ref);
if (!commit) {
return new NotFoundException(`Ref '${ref}' not found.`);
@ -33,30 +45,56 @@ export class ContentService {
if (path) {
try {
entries = [await commit.getEntry(path)];
const pathEntry = await commit.getEntry(path);
if (pathEntry.isTree()) {
const tree = await pathEntry.getTree();
// for directories, either
if (recursive) {
// recursively get children
entries = await this.getAllChildEntries(tree);
} else {
// get children immediate children
entries = tree.entries();
}
} else {
// for files get array of size 1
entries = [await commit.getEntry(path)];
}
} catch (e) {
return new NotFoundException(`${path} not found.`);
return new NotFoundException(`Path '${path}' not found.`);
}
} else {
const tree = await commit.getTree();
entries = await tree.entries();
entries = recursive ? await this.getAllChildEntries(tree) : tree.entries();
}
return this.getEntries(entries);
return this.getEntries(entries, includeContents);
}
private async getFileEntryAsObject(entry: TreeEntry): Promise<GitFileObjectContent> {
private async getFileEntryAsObject(
entry: TreeEntry,
includeContents: boolean,
): Promise<GitFileObjectWithContent | GitFileObjectWithoutContent> {
const blob = await entry.getBlob();
return new GitFileObjectContent({
const file = {
type: "file",
encoding: "base64",
size: blob.rawsize(),
name: entry.name(),
path: entry.path(),
content: blob.content().toString("base64"),
sha: entry.sha(),
});
};
if (includeContents) {
return new GitFileObjectWithContent({
...file,
content: blob.content().toString("base64"),
});
}
return new GitFileObjectWithoutContent(file);
}
private async getDirEntryAsObject(entry: TreeEntry): Promise<GitDirObjectContent> {
@ -78,15 +116,37 @@ export class ContentService {
});
}
private async getEntries(entries: TreeEntry[]): Promise<GitContents> {
private async getEntries(entries: TreeEntry[], includeContents: boolean): Promise<GitContents | GitTree> {
const [files, dirs, submodules] = await Promise.all([
Promise.all(entries.filter(entry => entry.isFile()).map(async entry => this.getFileEntryAsObject(entry))),
Promise.all(
entries.filter(entry => entry.isFile()).map(async entry => this.getFileEntryAsObject(entry, includeContents)),
),
Promise.all(entries.filter(entry => entry.isDirectory()).map(async entry => this.getDirEntryAsObject(entry))),
Promise.all(
entries.filter(entry => entry.isSubmodule()).map(async entry => this.getSubmoduleEntryAsObject(entry)),
),
]);
return new GitContents({ files, dirs, submodules });
if (includeContents) {
return new GitContents({ files, dirs, submodules });
}
return new GitTree({ files: files as GitFileObjectWithContent[], dirs, submodules });
}
private async getAllChildEntries(tree: Tree): Promise<TreeEntry[]> {
return new Promise((resolve, reject) => {
const eventEmitter = tree.walk(false);
eventEmitter.on("end", (trees: TreeEntry[]) => {
resolve(trees);
});
eventEmitter.on("error", error => {
reject(error);
});
eventEmitter.start();
});
}
}

Просмотреть файл

@ -1,5 +1,6 @@
export const notUndefined = <T>(x: T | undefined): x is T => x !== undefined;
export const delay = (timeout?: number) => new Promise(r => setTimeout(r, timeout));
export const parseBooleanFromURLParam = (bool: string | undefined) => bool === "" || bool === "true";
export class Deferred<T = void> {
public promise: Promise<T>;

Просмотреть файл

@ -271,6 +271,18 @@
"required": true,
"in": "path"
},
{
"name": "path",
"required": true,
"in": "path",
"type": "string"
},
{
"name": "recursive",
"required": false,
"in": "query",
"type": "string"
},
{
"name": "ref",
"required": false,
@ -311,6 +323,64 @@
"application/json"
]
}
},
"/repos/{remote}/tree/{path}": {
"get": {
"summary": "Get tree",
"operationId": "tree_get",
"parameters": [
{
"type": "string",
"name": "remote",
"required": true,
"in": "path"
},
{
"name": "path",
"required": true,
"in": "path",
"type": "string"
},
{
"name": "ref",
"required": false,
"in": "query",
"type": "string"
},
{
"name": "x-authorization",
"required": false,
"in": "header",
"type": "string"
},
{
"name": "x-github-token",
"required": false,
"in": "header",
"type": "string"
}
],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/GitTree"
}
},
"400": {
"description": "When the x-authorization header is malformed"
},
"404": {
"description": ""
}
},
"produces": [
"application/json"
],
"consumes": [
"application/json"
]
}
}
},
"definitions": {
@ -498,7 +568,7 @@
"sha"
]
},
"GitFileObjectContent": {
"GitFileObjectWithoutContent": {
"type": "object",
"properties": {
"type": {
@ -516,9 +586,6 @@
"sha": {
"type": "string"
},
"content": {
"type": "string"
},
"encoding": {
"type": "string"
}
@ -529,7 +596,6 @@
"name",
"path",
"sha",
"content",
"encoding"
]
},
@ -572,7 +638,70 @@
"files": {
"type": "array",
"items": {
"$ref": "#/definitions/GitFileObjectContent"
"$ref": "#/definitions/GitFileObjectWithoutContent"
}
},
"submodules": {
"type": "array",
"items": {
"$ref": "#/definitions/GitSubmoduleObjectContent"
}
}
},
"required": [
"dirs",
"files",
"submodules"
]
},
"GitFileObjectWithContent": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"size": {
"type": "number"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"sha": {
"type": "string"
},
"encoding": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": [
"type",
"size",
"name",
"path",
"sha",
"encoding",
"content"
]
},
"GitTree": {
"type": "object",
"properties": {
"dirs": {
"type": "array",
"items": {
"$ref": "#/definitions/GitDirObjectContent"
}
},
"files": {
"type": "array",
"items": {
"$ref": "#/definitions/GitFileObjectWithContent"
}
},
"submodules": {