зеркало из https://github.com/Azure/git-rest-api.git
Feature: Basic repo clone and fetch list branches for a git repo (#3)
This commit is contained in:
Родитель
8a0b0038e5
Коммит
ee93476917
|
@ -58,4 +58,5 @@ jspm_packages/
|
|||
.next
|
||||
|
||||
bin/
|
||||
buildcache/
|
||||
buildcache/
|
||||
tmp/
|
|
@ -6,8 +6,9 @@
|
|||
"request": "launch",
|
||||
"name": "Run server",
|
||||
"program": "${workspaceFolder}/src/main.ts",
|
||||
"outFiles": ["${workspaceFolder}/bin/main.js"],
|
||||
"outFiles": ["${workspaceFolder}/bin/**/*.js"],
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": ["node_modules", "bin", "test", "**/*spec.ts"]
|
||||
"exclude": ["node_modules", "bin", "test", "**/*.test.ts"]
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ const config = {
|
|||
tsConfig: "tsconfig.json",
|
||||
},
|
||||
},
|
||||
setupFiles: ["<rootDir>/test/jest-setup.ts"],
|
||||
testMatch: ["**/*.test.ts"],
|
||||
verbose: true,
|
||||
testEnvironment: "node",
|
||||
|
|
|
@ -417,6 +417,22 @@
|
|||
"multer": "1.4.1"
|
||||
}
|
||||
},
|
||||
"@nestjs/swagger": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-3.0.2.tgz",
|
||||
"integrity": "sha512-7cmOqa3MoK3ZXThECa3RCe5s5Bppm66DqDRz+nfTp5k2oGoFHX9t45jNU8P5aANCIEi1nA3TYwvEAGgZdV8WbA==",
|
||||
"requires": {
|
||||
"lodash": "4.17.11",
|
||||
"path-to-regexp": "3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"path-to-regexp": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.0.0.tgz",
|
||||
"integrity": "sha512-ZOtfhPttCrqp2M1PBBH4X13XlvnfhIwD7yCLx+GoGoXRPQyxGOTdQMpIzPSPKXAJT/JQrdfFrgdJOyAzvgpQ9A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nuxtjs/opencollective": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.2.1.tgz",
|
||||
|
@ -513,6 +529,15 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
|
||||
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA=="
|
||||
},
|
||||
"@types/nodegit": {
|
||||
"version": "0.24.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodegit/-/nodegit-0.24.6.tgz",
|
||||
"integrity": "sha512-W4HgaAR/eV2JIabXGmfMkad2SxoXVveC+NzTLzRXP8a1/uxIBCdoULAH9N7MWXQjZ9foiykAVd/LZ9cXdx5tog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||
|
@ -3322,6 +3347,22 @@
|
|||
"supports-color": "^6.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pify": "^4.0.1",
|
||||
"semver": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
|
||||
|
@ -3355,11 +3396,27 @@
|
|||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pify": "^4.0.1",
|
||||
"semver": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -4010,20 +4067,17 @@
|
|||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
||||
"dev": true,
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz",
|
||||
"integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==",
|
||||
"requires": {
|
||||
"pify": "^4.0.1",
|
||||
"semver": "^5.6.0"
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||
"dev": true
|
||||
"semver": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz",
|
||||
"integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5689,6 +5743,21 @@
|
|||
"has-flag": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"swagger-ui-dist": {
|
||||
"version": "3.22.1",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.22.1.tgz",
|
||||
"integrity": "sha512-KITbEqXkXrjGH12A0lpVZlH3uODFkwUh8d15My1YD4N0PSZDnIiC1iMFT6ryyuJxDYWZh0qezKpPqa5FRowngw==",
|
||||
"dev": true
|
||||
},
|
||||
"swagger-ui-express": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.0.3.tgz",
|
||||
"integrity": "sha512-G0anPbcUHCnf84fum1BPZ2s1LG4DP1bYBFgF05YB0ibXx4VMAkGIMSq7Y9yT8hMtTaKkmK+ikJFOCCeA9FcRfA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"swagger-ui-dist": "^3.18.1"
|
||||
}
|
||||
},
|
||||
"symbol-tree": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz",
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"start:debug": "nodemon --config nodemon-debug.json",
|
||||
"test": "jest",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"test:watch": "jest --watch --coverage:false --config ./config/jest.dev.config.js"
|
||||
"test:watch": "jest --watch --coverage=false --config ./config/jest.dev.config.js",
|
||||
"swagger:gen": "node ./bin/scripts/generate-swagger-specs.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -26,9 +27,11 @@
|
|||
"homepage": "https://github.com/Azure/git-rest-api#readme",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.13",
|
||||
"@types/nodegit": "^0.24.6",
|
||||
"concurrently": "^4.1.0",
|
||||
"jest": "^24.8.0",
|
||||
"prettier": "^1.17.1",
|
||||
"swagger-ui-express": "^4.0.3",
|
||||
"ts-jest": "^24.0.2",
|
||||
"tslint": "^5.16.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
|
@ -39,8 +42,10 @@
|
|||
"@nestjs/common": "^6.2.0",
|
||||
"@nestjs/core": "^6.2.0",
|
||||
"@nestjs/platform-express": "^6.2.0",
|
||||
"@nestjs/swagger": "^3.0.2",
|
||||
"@types/node": "^12.0.2",
|
||||
"class-validator": "^0.9.1",
|
||||
"make-dir": "^3.0.0",
|
||||
"nodegit": "^0.24.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^6.5.2"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { AppController } from "./controllers";
|
||||
import { AppService } from "./services";
|
||||
import { AppController, BranchesController, HealthCheckController } from "./controllers";
|
||||
import { AppService, BranchService, FSService, PermissionService, RepoService } from "./services";
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
controllers: [AppController, HealthCheckController, BranchesController],
|
||||
providers: [AppService, RepoService, FSService, BranchService, PermissionService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { NestFactory } from "@nestjs/core";
|
||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
export async function createApp() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const options = new DocumentBuilder()
|
||||
.setTitle("GIT Rest API")
|
||||
.setDescription("Rest api to run operation on git repositories")
|
||||
.setVersion("1.0")
|
||||
.setSchemes("http", "https")
|
||||
.build();
|
||||
const document = SwaggerModule.createDocument(app, options);
|
||||
SwaggerModule.setup("swagger", app, document);
|
||||
return { app, document };
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { RepoAuth } from "../../core";
|
||||
import { BranchesController } from "./branches.controller";
|
||||
|
||||
const b1 = {
|
||||
name: "master",
|
||||
commit: {
|
||||
sha: "sha1",
|
||||
},
|
||||
};
|
||||
|
||||
const b2 = {
|
||||
name: "master",
|
||||
commit: {
|
||||
sha: "sha2",
|
||||
},
|
||||
};
|
||||
|
||||
describe("BranchController", () => {
|
||||
let controller: BranchesController;
|
||||
const branchServiceSpy = {
|
||||
list: jest.fn(() => [b1, b2]),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
controller = new BranchesController(branchServiceSpy as any);
|
||||
});
|
||||
|
||||
it("list the branches", async () => {
|
||||
const auth = new RepoAuth({});
|
||||
const branches = await controller.list("github.com/Azure/git-rest-api", auth);
|
||||
|
||||
expect(branches).toEqual([b1, b2]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import { Controller, Get, Param } from "@nestjs/common";
|
||||
import { ApiNotFoundResponse, ApiOkResponse } from "@nestjs/swagger";
|
||||
|
||||
import { ApiHasPassThruAuth, Auth, RepoAuth } from "../../core";
|
||||
import { GitBranch } from "../../dtos";
|
||||
import { BranchService } from "../../services";
|
||||
|
||||
@Controller("/repos/:remote/branches")
|
||||
export class BranchesController {
|
||||
constructor(private branchService: BranchService) {}
|
||||
|
||||
@Get()
|
||||
@ApiHasPassThruAuth()
|
||||
@ApiOkResponse({ type: GitBranch, isArray: true })
|
||||
@ApiNotFoundResponse({})
|
||||
public async list(@Param("remote") remote: string, @Auth() auth: RepoAuth): Promise<GitBranch[]> {
|
||||
const branches = await this.branchService.list(remote, { auth });
|
||||
return branches;
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./app.controller";
|
||||
export * from "./health-check/health-check.controller";
|
||||
export * from "./branches/branches.controller";
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from "./repo-auth";
|
|
@ -0,0 +1 @@
|
|||
export * from "./repo-auth";
|
|
@ -0,0 +1,17 @@
|
|||
import { Cred } from "nodegit";
|
||||
|
||||
import { RepoAuth } from "./repo-auth";
|
||||
|
||||
describe("RepoAuth", () => {
|
||||
describe("Model", () => {
|
||||
it("doesn't generated any creds when no options are passed", () => {
|
||||
expect(new RepoAuth({}).toCreds()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("doesn't generate some basic password creds when using oath token", () => {
|
||||
expect(new RepoAuth({ token: "token-1" }).toCreds()).toEqual(
|
||||
Cred.userpassPlaintextNew("token-1", "x-oauth-basic"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
import { createParamDecorator } from "@nestjs/common";
|
||||
import { ApiImplicitHeaders } from "@nestjs/swagger";
|
||||
import { Cred } from "nodegit";
|
||||
|
||||
export interface IRepoAuth {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export class RepoAuth {
|
||||
private token: string | undefined;
|
||||
|
||||
constructor(obj: IRepoAuth) {
|
||||
this.token = obj.token;
|
||||
}
|
||||
|
||||
public toCreds(): Cred | undefined {
|
||||
if (this.token) {
|
||||
return Cred.userpassPlaintextNew(this.token, "x-oauth-basic");
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TOKEN_HEADER = "x-oauth-basic";
|
||||
|
||||
export const Auth = createParamDecorator(
|
||||
(_, req): RepoAuth => {
|
||||
return new RepoAuth({
|
||||
token: req.headers[TOKEN_HEADER],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper to add on methods using the Auth parameter for the swagger specs to be generated correctly
|
||||
*/
|
||||
export function ApiHasPassThruAuth() {
|
||||
return ApiImplicitHeaders([
|
||||
{
|
||||
name: TOKEN_HEADER,
|
||||
required: false,
|
||||
},
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { ApiModelProperty } from "@nestjs/swagger";
|
||||
|
||||
import { GitCommit, IGitCommit } from "./git-commit";
|
||||
|
||||
export interface IGitBranch {
|
||||
name: string;
|
||||
commit: IGitCommit;
|
||||
}
|
||||
|
||||
export class GitBranch implements IGitBranch {
|
||||
@ApiModelProperty({ type: String })
|
||||
public name: string;
|
||||
|
||||
@ApiModelProperty({ type: GitCommit })
|
||||
public commit: GitCommit;
|
||||
|
||||
constructor(branch: IGitBranch) {
|
||||
this.name = branch.name;
|
||||
this.commit = new GitCommit(branch.commit);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { ApiModelProperty } from "@nestjs/swagger";
|
||||
|
||||
export interface IGitCommit {
|
||||
sha: string;
|
||||
}
|
||||
|
||||
export class GitCommit implements IGitCommit {
|
||||
@ApiModelProperty({ type: String })
|
||||
public sha: string;
|
||||
|
||||
constructor(commit: { sha: string }) {
|
||||
this.sha = commit.sha;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./git-branch";
|
||||
export * from "./git-commit";
|
|
@ -1,9 +1,7 @@
|
|||
import { NestFactory } from "@nestjs/core";
|
||||
|
||||
import { AppModule } from "./app.module";
|
||||
import { createApp } from "./app";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const { app } = await createApp();
|
||||
await app.listen(3009);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { saveSwagger } from "./swagger-generation";
|
||||
|
||||
void saveSwagger().then(() => {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log("Generated the specs");
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import fs from "fs";
|
||||
import { promisify } from "util";
|
||||
|
||||
import { createApp } from "../app";
|
||||
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
const readFile = promisify(fs.readFile);
|
||||
|
||||
const SWAGGER_FILE_PATH = "./swagger-spec.json";
|
||||
|
||||
export async function generateSwagger(): Promise<string> {
|
||||
const { document } = await createApp();
|
||||
return JSON.stringify(document, null, 2);
|
||||
}
|
||||
|
||||
export async function saveSwagger() {
|
||||
const specs = await generateSwagger();
|
||||
await writeFile(SWAGGER_FILE_PATH, specs);
|
||||
}
|
||||
|
||||
export async function getSavedSwagger(): Promise<string> {
|
||||
const response = await readFile(SWAGGER_FILE_PATH);
|
||||
return response.toString();
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { generateSwagger, getSavedSwagger } from "./swagger-generation";
|
||||
|
||||
describe("Swagger specs", () => {
|
||||
it("should be up to date", async () => {
|
||||
const [specs, current] = await Promise.all([generateSwagger(), getSavedSwagger()]);
|
||||
|
||||
// Swagger specs saved in the repo should match the ones being generated run `npm run swagger:gen` to regenerate
|
||||
expect(specs).toEqual(current);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import { GitBranch } from "../../dtos";
|
||||
import { BranchService } from "./branch.service";
|
||||
|
||||
const b1 = new GitBranch({
|
||||
name: "master",
|
||||
commit: {
|
||||
sha: "sha1",
|
||||
},
|
||||
});
|
||||
|
||||
const b2 = new GitBranch({
|
||||
name: "stable",
|
||||
commit: {
|
||||
sha: "sha2",
|
||||
},
|
||||
});
|
||||
|
||||
const refs = [
|
||||
{ isRemote: () => true, name: () => "refs/remotes/origin/master", target: () => "sha1" },
|
||||
{ isRemote: () => false, name: () => "refs/head/feat1", target: () => "sha3" },
|
||||
{ isRemote: () => true, name: () => "refs/remotes/origin/stable", target: () => "sha2" },
|
||||
];
|
||||
|
||||
describe("BranchService", () => {
|
||||
let service: BranchService;
|
||||
|
||||
const mockRepo = {
|
||||
getReferences: jest.fn(() => {
|
||||
return refs;
|
||||
}),
|
||||
};
|
||||
|
||||
const repoServiceSpy = {
|
||||
get: jest.fn(() => mockRepo),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new BranchService(repoServiceSpy as any);
|
||||
});
|
||||
|
||||
it("List the branches", async () => {
|
||||
const branches = await service.list("github.com/Azure/git-rest-api");
|
||||
|
||||
expect(repoServiceSpy.get).toHaveBeenCalledTimes(1);
|
||||
expect(repoServiceSpy.get).toHaveBeenCalledWith("github.com/Azure/git-rest-api", {});
|
||||
expect(mockRepo.getReferences).toHaveBeenCalledTimes(1);
|
||||
expect(branches).toEqual([b1, b2]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { Reference } from "nodegit";
|
||||
|
||||
import { GitBranch } from "../../dtos";
|
||||
import { GitBaseOptions, RepoService } from "../repo";
|
||||
|
||||
@Injectable()
|
||||
export class BranchService {
|
||||
constructor(private repoService: RepoService) {}
|
||||
|
||||
/**
|
||||
* List the branches
|
||||
* @param remote
|
||||
*/
|
||||
public async list(remote: string, options: GitBaseOptions = {}): Promise<GitBranch[]> {
|
||||
const repo = await this.repoService.get(remote, options);
|
||||
const refs = await repo.getReferences(Reference.TYPE.LISTALL);
|
||||
const branches = refs.filter(x => x.isRemote());
|
||||
|
||||
return Promise.all(
|
||||
branches.map(async ref => {
|
||||
const target = await ref.target();
|
||||
return new GitBranch({
|
||||
name: getRemoteBranchName(ref.name()),
|
||||
commit: {
|
||||
sha: target.toString(),
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteBranchName(refName: string) {
|
||||
const prefix = "refs/remotes/origin/";
|
||||
return refName.slice(prefix.length);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./branch.service";
|
|
@ -0,0 +1,19 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import fs from "fs";
|
||||
import makeDir from "make-dir";
|
||||
import util from "util";
|
||||
|
||||
const fsPromises = {
|
||||
exists: util.promisify(fs.exists),
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class FSService {
|
||||
public async exists(path: string): Promise<boolean> {
|
||||
return fsPromises.exists(path);
|
||||
}
|
||||
|
||||
public async makeDir(path: string): Promise<string> {
|
||||
return makeDir(path);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./fs.service";
|
|
@ -1 +1,5 @@
|
|||
export * from "./app.service";
|
||||
export * from "./branch";
|
||||
export * from "./fs";
|
||||
export * from "./repo";
|
||||
export * from "./permission";
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from "./permission.service";
|
|
@ -0,0 +1,28 @@
|
|||
import { PermissionService, TokenPermission } from "./permission.service";
|
||||
|
||||
const remote = "github.com/Azure/git-rest-specs";
|
||||
describe("PermissionService", () => {
|
||||
let service: PermissionService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.useFakeTimers();
|
||||
service = new PermissionService();
|
||||
});
|
||||
|
||||
it("returns undefined when permission has not been set", () => {
|
||||
expect(service.getTokenPermission("token-1", remote)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("set a permission", () => {
|
||||
service.setTokenPermission("token-1", remote, TokenPermission.Read);
|
||||
expect(service.getTokenPermission("token-1", remote)).toBe(TokenPermission.Read);
|
||||
});
|
||||
|
||||
it("clear permission after a timeout", () => {
|
||||
service.setTokenPermission("token-1", remote, TokenPermission.Read);
|
||||
expect(service.getTokenPermission("token-1", remote)).toBe(TokenPermission.Read);
|
||||
jest.runTimersToTime(60_000);
|
||||
expect(service.getTokenPermission("token-1", remote)).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { SecureUtils } from "../../utils";
|
||||
|
||||
export enum TokenPermission {
|
||||
None,
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
|
||||
const TOKEN_INVALIDATE_TIMEOUT = 60_000; // 60s
|
||||
|
||||
@Injectable()
|
||||
export class PermissionService {
|
||||
private tokenPermissions = new Map<string, TokenPermission>();
|
||||
private timeouts = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
public getTokenPermission(token: string, remote: string): TokenPermission | undefined {
|
||||
return this.tokenPermissions.get(this.getMapKey(token, remote));
|
||||
}
|
||||
|
||||
public setTokenPermission(token: string, remote: string, permission: TokenPermission) {
|
||||
const key = this.getMapKey(token, remote);
|
||||
this.tokenPermissions.set(key, permission);
|
||||
this.timeoutPermission(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the cached permission after a timeout
|
||||
*/
|
||||
private timeoutPermission(key: string) {
|
||||
const existingTimeout = this.timeouts.get(key);
|
||||
if (existingTimeout) {
|
||||
clearTimeout(existingTimeout);
|
||||
this.timeouts.delete(key);
|
||||
}
|
||||
|
||||
this.timeouts.set(
|
||||
key,
|
||||
setTimeout(() => {
|
||||
this.tokenPermissions.delete(key);
|
||||
this.timeouts.delete(key);
|
||||
}, TOKEN_INVALIDATE_TIMEOUT),
|
||||
);
|
||||
}
|
||||
|
||||
private getMapKey(token: string, remote: string) {
|
||||
return `${this.hashToken(token)}/${remote}`;
|
||||
}
|
||||
|
||||
private hashToken(token: string): string {
|
||||
return SecureUtils.sha512(token);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./repo.service";
|
|
@ -0,0 +1,77 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Clone, Fetch, FetchOptions, Repository } from "nodegit";
|
||||
import path from "path";
|
||||
|
||||
import { RepoAuth } from "../../core";
|
||||
import { FSService } from "../fs";
|
||||
|
||||
const repoCacheFolder = path.join("./tmp", "repos");
|
||||
export function getRepoMainPath(remote: string) {
|
||||
return path.join(repoCacheFolder, encodeURIComponent(remote));
|
||||
}
|
||||
|
||||
const defaultFetchOptions: FetchOptions = {
|
||||
downloadTags: 0,
|
||||
prune: Fetch.PRUNE.GIT_FETCH_PRUNE,
|
||||
};
|
||||
|
||||
export interface GitBaseOptions {
|
||||
auth?: RepoAuth;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RepoService {
|
||||
private cacheReady: Promise<string>;
|
||||
constructor(private fs: FSService) {
|
||||
this.cacheReady = fs.makeDir(repoCacheFolder);
|
||||
}
|
||||
|
||||
public async get(remote: string, options: GitBaseOptions = {}): Promise<Repository> {
|
||||
const repoPath = getRepoMainPath(remote);
|
||||
if (await this.fs.exists(repoPath)) {
|
||||
const repo = await Repository.open(repoPath);
|
||||
await this.fetchAll(repo, options);
|
||||
return repo;
|
||||
} else {
|
||||
return this.clone(remote, repoPath, options);
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchAll(repo: Repository, options: GitBaseOptions) {
|
||||
try {
|
||||
await repo.fetchAll({
|
||||
...defaultFetchOptions,
|
||||
callbacks: {
|
||||
credentials: credentialsCallback(options),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
public async clone(remote: string, repoPath: string, options: GitBaseOptions): Promise<Repository> {
|
||||
await this.cacheReady;
|
||||
try {
|
||||
return await Clone.clone(`https://${remote}`, repoPath, {
|
||||
fetchOpts: {
|
||||
...defaultFetchOptions,
|
||||
callbacks: {
|
||||
credentials: credentialsCallback(options),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function credentialsCallback(options: GitBaseOptions) {
|
||||
return () => {
|
||||
if (options.auth) {
|
||||
return options.auth.toCreds();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./secure-utils";
|
|
@ -0,0 +1,9 @@
|
|||
import crypto from "crypto";
|
||||
|
||||
export const SecureUtils = {
|
||||
sha512: (key: string) => {
|
||||
const hash = crypto.createHash("sha512");
|
||||
hash.update(key);
|
||||
return hash.digest("hex");
|
||||
},
|
||||
};
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "Rest api to run operation on git repositories",
|
||||
"version": "1.0",
|
||||
"title": "GIT Rest API"
|
||||
},
|
||||
"basePath": "/",
|
||||
"tags": [],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/health/alive": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/repos/{remote}/branches": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "remote",
|
||||
"required": true,
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"name": "x-oauth-basic",
|
||||
"required": false,
|
||||
"in": "header",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/GitBranch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"GitCommit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sha": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sha"
|
||||
]
|
||||
},
|
||||
"GitBranch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"commit": {
|
||||
"$ref": "#/definitions/GitCommit"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"commit"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
import "reflect-metadata";
|
|
@ -11,6 +11,7 @@
|
|||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"rootDir": "src",
|
||||
"outDir": "bin",
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
|
@ -44,5 +45,5 @@
|
|||
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "tmp", "bin"]
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче