diff --git a/docs/development-notes.md b/docs/development-notes.md new file mode 100644 index 0000000..c9cdb3a --- /dev/null +++ b/docs/development-notes.md @@ -0,0 +1,10 @@ +# Fennec CI/CD 工具开发手记 +## 前言 +这是 Fennec 后端项目开发手记,用于记录开发过程中遇到的知识点、难点、思路和解决方案。 + +## Git Repository 操作 +### 获取 git 远程仓库信息流程 +1. `git init` // 初始化一个本地 git 仓库。 +2. `git remote add
` // 添加远程仓库 +3. `git fetch` // 获取远程仓库信息 + diff --git a/package-lock.json b/package-lock.json index 5991cd3..4b1cd48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2804,6 +2804,14 @@ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" }, + "@types/ramda": { + "version": "0.27.38", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.38.tgz", + "integrity": "sha512-tZoQ0lv1WKkrpBHemL8yCkI9p8kUk/1PSMwhl0eeyqMQjD+2ePUtVLV8PpNS9Kq3OktObwOx9I3k+HumxTviRg==", + "requires": { + "ts-toolbelt": "^6.15.1" + } + }, "@types/range-parser": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", @@ -9957,6 +9965,11 @@ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, + "ramda": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -11806,6 +11819,11 @@ "yn": "3.1.1" } }, + "ts-toolbelt": { + "version": "6.15.5", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz", + "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==" + }, "tsconfig-paths": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", diff --git a/package.json b/package.json index 9aa40c0..6db8a02 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@nestjs/platform-express": "^7.5.1", "@nestjs/typeorm": "^7.1.5", "@neuralegion/class-sanitizer": "^0.3.2", + "@types/ramda": "^0.27.38", "apollo-server-express": "^2.19.2", "bcrypt": "^5.0.0", "class-transformer": "^0.3.2", @@ -36,6 +37,7 @@ "graphql-tools": "^7.0.2", "js-yaml": "^4.0.0", "pg": "^8.5.1", + "ramda": "^0.27.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^6.6.3", diff --git a/src/repos/repos.service.spec.ts b/src/repos/repos.service.spec.ts index 1759e1f..50e6605 100644 --- a/src/repos/repos.service.spec.ts +++ b/src/repos/repos.service.spec.ts @@ -3,26 +3,30 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Project } from '../projects/project.entity'; import { ReposService } from './repos.service'; import { ConfigService, ConfigModule } from '@nestjs/config'; -import { rm } from 'fs/promises'; +import { readFile, rm } from 'fs/promises'; import { join } from 'path'; import configuration from '../commons/config/configuration'; +import { NotFoundException } from '@nestjs/common'; + +const getTest1Project = () => + ({ + id: '1', + sshUrl: 'ssh://gitea@git.ivanli.cc:7018/Fennec/test-1.git', + name: 'test-1', + } as Project); 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', + ...getTest1Project(), ...entity, }), ), })); - let workspacesRoot: string; afterEach(async () => { - await rm(join(workspacesRoot, 'test1'), { + await rm(service.getWorkspaceRoot(getTest1Project()), { recursive: true, }).catch(() => undefined); }); @@ -43,17 +47,20 @@ describe('ReposService', () => { }).compile(); service = module.get(ReposService); - const configServer = module.get(ConfigService); - workspacesRoot = configServer.get('workspaces.root'); - await rm(join(workspacesRoot, 'test1'), { + await rm(service.getWorkspaceRoot(getTest1Project()), { recursive: true, - }).catch(() => undefined); + }).catch((err) => { + console.log('!!!!', err); + }); }); it('should be defined', () => { expect(service).toBeDefined(); }); + it('getWorkspaceRoot', () => { + expect(service.getWorkspaceRoot(getTest1Project())).toBeDefined(); + }); describe('listLogs', () => { it('should be return logs', async () => { const result = await service.listLogs({ projectId: '1' }); @@ -66,4 +73,61 @@ describe('ReposService', () => { expect(result).toBeDefined(); }, 10_000); }); + describe('checkoutBranch', () => { + it('should be checkout', async () => { + await service.checkoutBranch(getTest1Project(), 'master'); + const filePath = join( + service.getWorkspaceRoot(getTest1Project()), + 'README.md', + ); + const text = await readFile(filePath, { encoding: 'utf-8' }); + expect(text).toMatch(/Commit 1/gi); + }, 30_000); + it('multiplexing workspace', async () => { + await service.checkoutBranch(getTest1Project(), 'master'); + await service.checkoutBranch(getTest1Project(), 'branch-a'); + await service.checkoutBranch(getTest1Project(), 'branch-b'); + const filePath = join( + service.getWorkspaceRoot(getTest1Project()), + 'branch-b.md', + ); + const text = await readFile(filePath, { encoding: 'utf-8' }); + expect(text).toMatch(/Commit branch b/gi); + }, 30_000); + it('nonexistent branch', async () => { + return expect( + service.checkoutBranch(getTest1Project(), 'nonexistent'), + ).rejects.toBeInstanceOf(NotFoundException); + }, 30_000); + it('checkout the specified version', async () => { + await service.checkoutBranch(getTest1Project(), 'master'); + const filePath = join( + service.getWorkspaceRoot(getTest1Project()), + 'README.md', + ); + const text = await readFile(filePath, { encoding: 'utf-8' }); + expect(text).toMatch(/Commit 1/gi); + }, 30_000); + }); + + describe('checkoutCommit', () => { + it('should be checkout', async () => { + await service.checkoutCommit(getTest1Project(), '498c782685'); + const filePath = join( + service.getWorkspaceRoot(getTest1Project()), + 'README.md', + ); + const text = await readFile(filePath, { encoding: 'utf-8' }); + expect(text).toMatch(/Commit 1/gi); + }); + it('should be checkout right commit', async () => { + await service.checkoutCommit(getTest1Project(), '7f7123fe5b'); + const filePath = join( + service.getWorkspaceRoot(getTest1Project()), + 'README.md', + ); + const text = await readFile(filePath, { encoding: 'utf-8' }); + expect(text).toMatch(/(?!Commit 1)/gi); + }); + }); }); diff --git a/src/repos/repos.service.ts b/src/repos/repos.service.ts index b14a142..dd0454d 100644 --- a/src/repos/repos.service.ts +++ b/src/repos/repos.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { F_OK } from 'constants'; import { access, mkdir } from 'fs/promises'; @@ -9,8 +9,8 @@ import { Project } from '../projects/project.entity'; import { ListBranchesArgs } from './dtos/list-branches.args'; import { ListLogsArgs } from './dtos/list-logs.args'; import { ConfigService } from '@nestjs/config'; -import { log } from 'console'; +const DEFAULT_REMOTE_NAME = 'origin'; @Injectable() export class ReposService { constructor( @@ -19,21 +19,25 @@ export class ReposService { private readonly configService: ConfigService, ) {} - async getGit(project: Project) { - const workspacePath = join( + getWorkspaceRoot(project: Project): string { + return join( this.configService.get('workspaces.root'), project.name, ); - const firstInit = await access(workspacePath, F_OK) + } + + async getGit(project: Project) { + const workspaceRoot = this.getWorkspaceRoot(project); + const firstInit = await access(workspaceRoot, F_OK) .then(() => false) .catch(async () => { - await mkdir(workspacePath); + await mkdir(workspaceRoot); return true; }); - const git = gitP(workspacePath); + const git = gitP(workspaceRoot); if (firstInit) { await git.init(); - await git.addRemote('origin', project.sshUrl); + await git.addRemote(DEFAULT_REMOTE_NAME, project.sshUrl); } return git; } @@ -46,7 +50,7 @@ export class ReposService { await git.fetch(); return await git.log({ '--branches': dto.branch ?? '', - '--remotes': 'origin', + '--remotes': DEFAULT_REMOTE_NAME, }); } @@ -57,4 +61,34 @@ export class ReposService { const git = await this.getGit(project); return git.branch(); } + + async checkoutBranch(project: Project, branch: string) { + const git = await this.getGit(project); + try { + await git.fetch(DEFAULT_REMOTE_NAME, branch); + } catch (err) { + if (err.message.includes("couldn't find remote ref nonexistent")) { + throw new NotFoundException(err.message); + } + throw err; + } + await git.checkout([ + '-B', + branch, + '--track', + `${DEFAULT_REMOTE_NAME}/${branch}`, + ]); + } + + async checkoutCommit(project: Project, commitNumber: string) { + const git = await this.getGit(project); + try { + await git.checkout([commitNumber]); + } catch (err) { + if (err.message.includes("couldn't find remote ref nonexistent")) { + throw new NotFoundException(err.message); + } + throw err; + } + } }