From c4a58efd9d04149383d5f36e46e220abb8a2b01c Mon Sep 17 00:00:00 2001 From: billytrend Date: Tue, 23 Jul 2019 12:47:33 -0700 Subject: [PATCH] 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 --- package.json | 2 +- src/app.module.ts | 10 +- src/controllers/compare/compare.controller.ts | 2 +- .../content.controller.e2e.ts.snap | 237 ++++++++++++++++++ .../content/content.controller.e2e.ts | 15 ++ src/controllers/content/content.controller.ts | 21 +- .../__snapshots__/tree.controller.e2e.ts.snap | 179 +++++++++++++ src/controllers/tree/tree.controller.e2e.ts | 12 + src/controllers/tree/tree.controller.ts | 31 +++ src/dtos/git-contents.ts | 10 +- src/dtos/git-file-object-with-content.ts | 13 + ....ts => git-file-object-without-content.ts} | 7 +- src/dtos/git-tree.ts | 20 ++ src/dtos/index.ts | 12 +- src/services/commit/commit.service.ts | 3 +- src/services/compare/compare.service.ts | 3 +- src/services/content/content.service.ts | 100 ++++++-- src/utils/misc-utils.ts | 1 + swagger-spec.json | 141 ++++++++++- 19 files changed, 770 insertions(+), 49 deletions(-) create mode 100644 src/controllers/content/__snapshots__/content.controller.e2e.ts.snap create mode 100644 src/controllers/content/content.controller.e2e.ts create mode 100644 src/controllers/tree/__snapshots__/tree.controller.e2e.ts.snap create mode 100644 src/controllers/tree/tree.controller.e2e.ts create mode 100644 src/controllers/tree/tree.controller.ts create mode 100644 src/dtos/git-file-object-with-content.ts rename src/dtos/{git-file-object-content.ts => git-file-object-without-content.ts} (53%) create mode 100644 src/dtos/git-tree.ts diff --git a/package.json b/package.json index bc78086..b9aaf57 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/app.module.ts b/src/app.module.ts index d31a8d4..8ed78c9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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, diff --git a/src/controllers/compare/compare.controller.ts b/src/controllers/compare/compare.controller.ts index b038d6a..a550e76 100644 --- a/src/controllers/compare/compare.controller.ts +++ b/src/controllers/compare/compare.controller.ts @@ -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") diff --git a/src/controllers/content/__snapshots__/content.controller.e2e.ts.snap b/src/controllers/content/__snapshots__/content.controller.e2e.ts.snap new file mode 100644 index 0000000..70e27ea --- /dev/null +++ b/src/controllers/content/__snapshots__/content.controller.e2e.ts.snap @@ -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 [], +} +`; diff --git a/src/controllers/content/content.controller.e2e.ts b/src/controllers/content/content.controller.e2e.ts new file mode 100644 index 0000000..ed7fc27 --- /dev/null +++ b/src/controllers/content/content.controller.e2e.ts @@ -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(); + }, + ); +}); diff --git a/src/controllers/content/content.controller.ts b/src/controllers/content/content.controller.ts index 1540783..89e785c 100644 --- a/src/controllers/content/content.controller.ts +++ b/src/controllers/content/content.controller.ts @@ -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; } diff --git a/src/controllers/tree/__snapshots__/tree.controller.e2e.ts.snap b/src/controllers/tree/__snapshots__/tree.controller.e2e.ts.snap new file mode 100644 index 0000000..1050a64 --- /dev/null +++ b/src/controllers/tree/__snapshots__/tree.controller.e2e.ts.snap @@ -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 [], +} +`; diff --git a/src/controllers/tree/tree.controller.e2e.ts b/src/controllers/tree/tree.controller.e2e.ts new file mode 100644 index 0000000..31d408a --- /dev/null +++ b/src/controllers/tree/tree.controller.e2e.ts @@ -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(); + }); +}); diff --git a/src/controllers/tree/tree.controller.ts b/src/controllers/tree/tree.controller.ts new file mode 100644 index 0000000..c8d8f5e --- /dev/null +++ b/src/controllers/tree/tree.controller.ts @@ -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; + } +} diff --git a/src/dtos/git-contents.ts b/src/dtos/git-contents.ts index 7da978d..203ee89 100644 --- a/src/dtos/git-contents.ts +++ b/src/dtos/git-contents.ts @@ -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; diff --git a/src/dtos/git-file-object-with-content.ts b/src/dtos/git-file-object-with-content.ts new file mode 100644 index 0000000..60d44e9 --- /dev/null +++ b/src/dtos/git-file-object-with-content.ts @@ -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; + } +} diff --git a/src/dtos/git-file-object-content.ts b/src/dtos/git-file-object-without-content.ts similarity index 53% rename from src/dtos/git-file-object-content.ts rename to src/dtos/git-file-object-without-content.ts index bf839cc..af0c42d 100644 --- a/src/dtos/git-file-object-content.ts +++ b/src/dtos/git-file-object-without-content.ts @@ -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; } } diff --git a/src/dtos/git-tree.ts b/src/dtos/git-tree.ts new file mode 100644 index 0000000..a4a3ed5 --- /dev/null +++ b/src/dtos/git-tree.ts @@ -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; + } +} diff --git a/src/dtos/index.ts b/src/dtos/index.ts index 5e0e9b1..2755423 100644 --- a/src/dtos/index.ts +++ b/src/dtos/index.ts @@ -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"; diff --git a/src/services/commit/commit.service.ts b/src/services/commit/commit.service.ts index 2f6bd72..ac94a3b 100644 --- a/src/services/commit/commit.service.ts +++ b/src/services/commit/commit.service.ts @@ -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; diff --git a/src/services/compare/compare.service.ts b/src/services/compare/compare.service.ts index 9adb3fd..dfcd909 100644 --- a/src/services/compare/compare.service.ts +++ b/src/services/compare/compare.service.ts @@ -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"; diff --git a/src/services/content/content.service.ts b/src/services/content/content.service.ts index 2a9ca22..9653f4a 100644 --- a/src/services/content/content.service.ts +++ b/src/services/content/content.service.ts @@ -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 { + ): Promise { 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 { + private async getFileEntryAsObject( + entry: TreeEntry, + includeContents: boolean, + ): Promise { 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 { @@ -78,15 +116,37 @@ export class ContentService { }); } - private async getEntries(entries: TreeEntry[]): Promise { + private async getEntries(entries: TreeEntry[], includeContents: boolean): Promise { 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 { + 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(); + }); } } diff --git a/src/utils/misc-utils.ts b/src/utils/misc-utils.ts index 57f9f74..41df652 100644 --- a/src/utils/misc-utils.ts +++ b/src/utils/misc-utils.ts @@ -1,5 +1,6 @@ export const notUndefined = (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 { public promise: Promise; diff --git a/swagger-spec.json b/swagger-spec.json index 3c43b0b..37d22ed 100644 --- a/swagger-spec.json +++ b/swagger-spec.json @@ -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": {