diff --git a/.gitignore b/.gitignore index c16ef02..51fa54f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +workspaces/* +!workspaces/.gitkeep \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..86ff2ad --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "Repos" + ] +} \ No newline at end of file diff --git a/config.yml b/config.yml index 8a90312..ff3fc55 100644 --- a/config.yml +++ b/config.yml @@ -4,7 +4,7 @@ http: db: postgres: - host: 192.168.31.195 + host: 192.168.31.194 port: 5432 database: fennec username: fennec diff --git a/package-lock.json b/package-lock.json index 45393ff..5991cd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2056,6 +2056,34 @@ "chalk": "^4.0.0" } }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "requires": { + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, "@nestjs/cli": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-7.5.4.tgz", @@ -10753,6 +10781,31 @@ "resolved": "https://registry.npmjs.org/signedsource/-/signedsource-1.0.0.tgz", "integrity": "sha1-HdrOSYF5j5O9gzlzgD2A1S6TrWo=" }, + "simple-git": { + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.35.0.tgz", + "integrity": "sha512-VuXs2/HyZmZm43Z5IjvU+ahTmURh/Hmb/egmgNdFZuu8OEnW2emCalnL/4jRQkXeJvfzCTnev6wo5jtDmWw0Dw==", + "requires": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", diff --git a/package.json b/package.json index 64e4470..da2f17e 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^6.6.3", + "simple-git": "^2.35.0", "typeorm": "^0.2.30" }, "devDependencies": { diff --git a/src/app.module.ts b/src/app.module.ts index b7362d9..b22df05 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { AppController } from './app.controller'; import { AppResolver } from './app.resolver'; import { AppService } from './app.service'; import { ProjectsModule } from './projects/projects.module'; +import { ReposModule } from './repos/repos.module'; import configuration from './commons/config/configuration'; @Module({ @@ -37,6 +38,7 @@ import configuration from './commons/config/configuration'; inject: [ConfigService], }), ProjectsModule, + ReposModule, ], controllers: [AppController], providers: [AppService, AppResolver], diff --git a/src/projects/dtos/create-project.input.ts b/src/projects/dtos/create-project.input.ts index 93ce1c6..d43126b 100644 --- a/src/projects/dtos/create-project.input.ts +++ b/src/projects/dtos/create-project.input.ts @@ -19,7 +19,7 @@ export class CreateProjectInput { @MinLength(2) comment: string; - @IsUrl({ protocols: ['ssh'], require_protocol: false }) + @IsUrl({ protocols: ['ssh'] }) @MaxLength(256) sshUrl: string; diff --git a/src/projects/project.entity.ts b/src/projects/project.entity.ts index 07ec46d..611ddd4 100644 --- a/src/projects/project.entity.ts +++ b/src/projects/project.entity.ts @@ -1,6 +1,6 @@ import { ObjectType } from '@nestjs/graphql'; -import { AppBaseEntity } from 'src/commons/entities/app-base-entity'; import { Entity, Column, DeleteDateColumn } from 'typeorm'; +import { AppBaseEntity } from '../commons/entities/app-base-entity'; @ObjectType() @Entity() diff --git a/src/projects/projects.module.ts b/src/projects/projects.module.ts index daea7db..8dda814 100644 --- a/src/projects/projects.module.ts +++ b/src/projects/projects.module.ts @@ -7,5 +7,6 @@ import { Project } from './project.entity'; @Module({ imports: [TypeOrmModule.forFeature([Project])], providers: [ProjectsService, ProjectsResolver], + exports: [ProjectsService], }) export class ProjectsModule {} diff --git a/src/projects/projects.service.ts b/src/projects/projects.service.ts index 12390e6..a8bfe2d 100644 --- a/src/projects/projects.service.ts +++ b/src/projects/projects.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { BaseDbService } from 'src/commons/services/base-db.service'; +import { BaseDbService } from '../commons/services/base-db.service'; import { Repository } from 'typeorm'; import { CreateProjectInput } from './dtos/create-project.input'; import { Project } from './project.entity'; diff --git a/src/repos/dtos/branches-list.model.ts b/src/repos/dtos/branches-list.model.ts new file mode 100644 index 0000000..10f698e --- /dev/null +++ b/src/repos/dtos/branches-list.model.ts @@ -0,0 +1,24 @@ +import { ObjectType, Field } from '@nestjs/graphql'; +import { + LogResult, + DefaultLogFields, + BranchSummary, + BranchSummaryBranch, +} from 'simple-git'; + +@ObjectType() +export class Branch implements BranchSummaryBranch { + current: boolean; + name: string; + commit: string; + label: string; +} + +@ObjectType() +export class BranchesList { + detached: boolean; + current: string; + all: string[]; + @Field(() => [Branch]) + branches: Branch[]; +} diff --git a/src/repos/dtos/list-branches.args.ts b/src/repos/dtos/list-branches.args.ts new file mode 100644 index 0000000..fc910c4 --- /dev/null +++ b/src/repos/dtos/list-branches.args.ts @@ -0,0 +1,8 @@ +import { InputType } from '@nestjs/graphql'; +import { IsUUID } from 'class-validator'; + +@InputType() +export class ListBranchesArgs { + @IsUUID() + projectId: string; +} diff --git a/src/repos/dtos/list-logs.args.ts b/src/repos/dtos/list-logs.args.ts new file mode 100644 index 0000000..1eed6f2 --- /dev/null +++ b/src/repos/dtos/list-logs.args.ts @@ -0,0 +1,12 @@ +import { InputType, ObjectType } from '@nestjs/graphql'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; + +@InputType() +export class ListLogsArgs { + @IsUUID() + projectId: string; + + @IsString() + @IsOptional() + branch?: string; +} diff --git a/src/repos/dtos/logs-list.model.ts b/src/repos/dtos/logs-list.model.ts new file mode 100644 index 0000000..63a7f6a --- /dev/null +++ b/src/repos/dtos/logs-list.model.ts @@ -0,0 +1,21 @@ +import { ObjectType, Field } from '@nestjs/graphql'; +import { LogResult, DefaultLogFields } from 'simple-git'; + +@ObjectType() +export class LogFields { + hash: string; + date: string; + message: string; + refs: string; + body: string; + author_name: string; + author_email: string; +} + +@ObjectType() +export class LogsList implements LogResult { + @Field(() => [LogFields]) + all: LogFields[]; + total: number; + latest: LogFields; +} diff --git a/src/repos/repos.module.ts b/src/repos/repos.module.ts new file mode 100644 index 0000000..6922ecf --- /dev/null +++ b/src/repos/repos.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Project } from '../projects/project.entity'; +import { ReposResolver } from './repos.resolver'; +import { ReposService } from './repos.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Project])], + providers: [ReposResolver, ReposService], +}) +export class ReposModule {} diff --git a/src/repos/repos.resolver.spec.ts b/src/repos/repos.resolver.spec.ts new file mode 100644 index 0000000..161fd51 --- /dev/null +++ b/src/repos/repos.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ReposResolver } from './repos.resolver'; + +describe('ReposResolver', () => { + let resolver: ReposResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ReposResolver], + }).compile(); + + resolver = module.get(ReposResolver); + }); + +it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/src/repos/repos.resolver.ts b/src/repos/repos.resolver.ts new file mode 100644 index 0000000..d6fe32e --- /dev/null +++ b/src/repos/repos.resolver.ts @@ -0,0 +1,26 @@ +import { Args, Query, Resolver } from '@nestjs/graphql'; +import { ListLogsArgs } from './dtos/list-logs.args'; +import { ReposService } from './repos.service'; +import { LogsList } from './dtos/logs-list.model'; +import { ListBranchesArgs } from './dtos/list-branches.args'; +import { BranchesList } from './dtos/branches-list.model'; + +@Resolver() +export class ReposResolver { + con + @Query(() => LogsList) + async listLogs(@Args('listLogsArgs') dto: ListLogsArgs) { + return await this.service.listLogs(dto); + } + @Query(() => BranchesList) + async ListBranchesArgs( + @Args('listBranchesArgs') dto: ListBranchesArgs, + ): Promise { + return await this.service.listBranches(dto).then((data) => { + return { + ...data, + branches: Object.values(data.branches), + }; + }); + } +} diff --git a/src/repos/repos.service.spec.ts b/src/repos/repos.service.spec.ts new file mode 100644 index 0000000..790f3af --- /dev/null +++ b/src/repos/repos.service.spec.ts @@ -0,0 +1,48 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Project } from '../projects/project.entity'; +import { ReposService } from './repos.service'; + +describe('ReposService', () => { + let service: ReposService; + const repositoryMockFactory = jest.fn(() => ({ + findOneOrFail: jest.fn( + (entity): Project => ({ + id: '1', + // sshUrl: 'ssh://gitea@git.ivanli.cc:7018/ivan/test1.git', + sshUrl: 'ssh://gitea@git.ivanli.cc:7018/Fennec/fennec-fe.git', + name: 'test1', + ...entity, + }), + ), + })); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ReposService, + { + provide: getRepositoryToken(Project), + useFactory: repositoryMockFactory, + }, + ], + }).compile(); + + service = module.get(ReposService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + describe('listLogs', () => { + it('should be return logs', async () => { + const result = await service.listLogs({ projectId: '1' }); + expect(result).toBeDefined(); + }, 10_000); + }); + describe('listBranch', () => { + it('should be return branches', async () => { + const result = await service.listBranches({ projectId: '1' }); + expect(result).toBeDefined(); + }, 10_000); + }); +}); diff --git a/src/repos/repos.service.ts b/src/repos/repos.service.ts new file mode 100644 index 0000000..642d224 --- /dev/null +++ b/src/repos/repos.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { F_OK } from 'constants'; +import { access, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { gitP } from 'simple-git'; +import { Repository } from 'typeorm'; +import { Project } from '../projects/project.entity'; +import { ListBranchesArgs } from './dtos/list-branches.args'; +import { ListLogsArgs } from './dtos/list-logs.args'; + +@Injectable() +export class ReposService { + constructor( + @InjectRepository(Project) + private readonly projectRepository: Repository, + ) {} + + async getGit(project: Project) { + const workspacePath = join(__dirname, '../../workspaces', project.name); + await access(workspacePath, F_OK).catch(() => mkdir(workspacePath)); + return gitP(workspacePath); + } + + async listLogs(dto: ListLogsArgs) { + const project = await this.projectRepository.findOneOrFail({ + id: dto.projectId, + }); + const git = await this.getGit(project); + await git.fetch(); + return git.log(); + } + + async listBranches(dto: ListBranchesArgs) { + const project = await this.projectRepository.findOneOrFail({ + id: dto.projectId, + }); + const git = await this.getGit(project); + return git.branch(); + } +} diff --git a/workspaces/.gitkeep b/workspaces/.gitkeep new file mode 100644 index 0000000..e69de29