Make methods as thin wrappers to Git executable. Also pass the child process for easy stream operations to consumers.master
@@ -82,8 +82,6 @@ | |||||
"access": "public" | "access": "public" | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@prisma/client": "^3.14.0", | |||||
"@theoryofnekomata/uuid-buffer": "^0.1.0", | |||||
"prisma": "^3.14.0" | |||||
"@theoryofnekomata/uuid-buffer": "^0.1.0" | |||||
} | } | ||||
} | } |
@@ -1,70 +0,0 @@ | |||||
// This is your Prisma schema file, | |||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema | |||||
generator client { | |||||
provider = "prisma-client-js" | |||||
} | |||||
datasource db { | |||||
provider = "sqlite" | |||||
url = env("DATABASE_URL") | |||||
} | |||||
model User { | |||||
id Bytes @id | |||||
username String @unique | |||||
password String | |||||
createdAt DateTime @default(now()) | |||||
properties Property[] | |||||
userOrgs UserOrg[] | |||||
} | |||||
model Property { | |||||
id Bytes @id | |||||
userId Bytes | |||||
user User @relation(fields: [userId], references: [id]) | |||||
name String | |||||
value String | |||||
} | |||||
model Org { | |||||
id Bytes @id | |||||
name String @unique | |||||
description String | |||||
createdAt DateTime @default(now()) | |||||
orgUsers UserOrg[] | |||||
} | |||||
model UserOrg { | |||||
id Int @id @default(autoincrement()) | |||||
userId Bytes | |||||
user User @relation(fields: [userId], references: [id]) | |||||
orgId Bytes | |||||
org Org @relation(fields: [orgId], references: [id]) | |||||
} | |||||
model Repo { | |||||
id Bytes @id | |||||
name String | |||||
visibility Int | |||||
ownerId Bytes | |||||
ownerType Int | |||||
createdAt DateTime @default(now()) | |||||
} | |||||
model Log { | |||||
id Int @id @default(autoincrement()) | |||||
subjectUserId Bytes | |||||
subjectUsername String | |||||
action String | |||||
createdAt DateTime @default(now()) | |||||
parameters LogParameter[] | |||||
} | |||||
model LogParameter { | |||||
id Int @id @default(autoincrement()) | |||||
logId Int | |||||
log Log @relation(fields: [logId], references: [id]) | |||||
key String | |||||
value String | |||||
} |
@@ -1,2 +1 @@ | |||||
export * as common from './modules/common'; | |||||
export * as git from './modules/git'; | export * as git from './modules/git'; |
@@ -1 +0,0 @@ | |||||
export * from './models'; |
@@ -1,12 +0,0 @@ | |||||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||||
export type User = { | |||||
id: Uuid, | |||||
username: string, | |||||
} | |||||
export type Org = { | |||||
id: Uuid, | |||||
name: string, | |||||
description: string, | |||||
} |
@@ -1,133 +1,58 @@ | |||||
import * as path from 'path'; | |||||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||||
import {PrismaClient} from '@prisma/client'; | |||||
import {ChildProcess} from 'child_process'; | |||||
import {spawn} from '../../utils/process'; | import {spawn} from '../../utils/process'; | ||||
import {mkdirp, unlink} from '../../utils/fs'; | import {mkdirp, unlink} from '../../utils/fs'; | ||||
import {User} from '../common'; | |||||
import {LogService, LogServiceImpl} from '../log'; | |||||
import * as models from './models'; | |||||
import {notFoundFactory} from '../../utils/error'; | |||||
export interface GitService { | export interface GitService { | ||||
create(data: models.CreateRepoData, subject: User): Promise<models.Repo> | |||||
delete(repoId: models.Repo['id'], subject: User): Promise<void> | |||||
create(repoBasePath: string): Promise<ChildProcess> | |||||
delete(repoBasePath: string): Promise<void> | |||||
advertiseReceivePackRefs(repoBasePath: string): Promise<ChildProcess> | |||||
advertiseUploadPackRefs(repoBasePath: string): Promise<ChildProcess> | |||||
receivePack(repoBasePath: string): Promise<ChildProcess> | |||||
uploadPack(repoBasePath: string): Promise<ChildProcess> | |||||
} | } | ||||
export class GitServiceImpl implements GitService { | export class GitServiceImpl implements GitService { | ||||
private readonly prismaClient: PrismaClient | |||||
private readonly logService: LogService | |||||
constructor() { | |||||
this.prismaClient = new PrismaClient(); | |||||
this.logService = new LogServiceImpl() | |||||
private static isWindows() { | |||||
return /^win/.test(process.platform); | |||||
} | } | ||||
private static getRepoBasePath(repoIdString: string) { | |||||
return path.join('repos', repoIdString) | |||||
async create(repoBasePath: string): Promise<ChildProcess> { | |||||
await mkdirp(repoBasePath); | |||||
return spawn( | |||||
repoBasePath, | |||||
'git', ['init', '--bare'], | |||||
); | |||||
} | } | ||||
private async getOwnerName(ownerId: Uuid, ownerType: models.OwnerType) { | |||||
let owner: models.Owner | |||||
if (ownerType === models.OwnerType.USER) { | |||||
owner = await this.prismaClient.user.findUnique({ | |||||
where: { | |||||
id: ownerId, | |||||
}, | |||||
rejectOnNotFound: notFoundFactory(models.UserNotFoundError), | |||||
}) | |||||
return owner.username; | |||||
} | |||||
if (ownerType === models.OwnerType.ORG) { | |||||
owner = await this.prismaClient.org.findUnique({ | |||||
where: { | |||||
id: ownerId, | |||||
}, | |||||
rejectOnNotFound: notFoundFactory(models.OrgNotFoundError), | |||||
}) | |||||
return owner.name; | |||||
} | |||||
throw new models.UnknownOwnerTypeError('Unknown owner type.') | |||||
async delete(repoBasePath: string): Promise<void> { | |||||
await unlink(repoBasePath); | |||||
} | } | ||||
private static async createRepoFiles(repoIdString: string) { | |||||
const repoBasePath = GitServiceImpl.getRepoBasePath(repoIdString); | |||||
await mkdirp(repoBasePath); | |||||
await spawn(repoBasePath, 'git', ['init', '--bare']); | |||||
async advertiseReceivePackRefs(repoBasePath: string): Promise<ChildProcess> { | |||||
const command = GitServiceImpl.isWindows() ? 'git' : 'git-receive-pack'; | |||||
const commonArgs = ['--stateless-rpc', '--advertise-refs', '.']; | |||||
const args = GitServiceImpl.isWindows() ? ['receive-pack', ...commonArgs] : commonArgs; | |||||
return spawn(repoBasePath, command, args); | |||||
} | } | ||||
async create(data: models.CreateRepoData, subject: User): Promise<models.Repo> { | |||||
const repoId = Uuid.v4(); | |||||
const repoIdString = repoId.toString() | |||||
await GitServiceImpl.createRepoFiles(repoIdString); | |||||
const repoMetadata = await this.prismaClient.repo.create({ | |||||
data: { | |||||
id: repoId, | |||||
name: data.name, | |||||
visibility: data.visibility, | |||||
ownerId: data.ownerId, | |||||
ownerType: data.ownerType, | |||||
}, | |||||
}); | |||||
const ownerName = await this.getOwnerName(data.ownerId, data.ownerType); | |||||
await this.logService.create( | |||||
subject, | |||||
models.GitAction.REPO_CREATED, | |||||
{ | |||||
key: 'repoId', | |||||
value: repoIdString, | |||||
}, | |||||
{ | |||||
key: 'repoName', | |||||
value: data.name, | |||||
}, | |||||
{ | |||||
key: 'ownerId', | |||||
value: data.ownerId.toString(), | |||||
}, | |||||
{ | |||||
key: 'ownerName', | |||||
value: ownerName, | |||||
}, | |||||
); | |||||
return { | |||||
...repoMetadata, | |||||
id: Uuid.from(repoMetadata.id), | |||||
}; | |||||
async advertiseUploadPackRefs(repoBasePath: string): Promise<ChildProcess> { | |||||
const command = GitServiceImpl.isWindows() ? 'git' : 'git-upload-pack'; | |||||
const commonArgs = ['--stateless-rpc', '--advertise-refs', '.']; | |||||
const args = GitServiceImpl.isWindows() ? ['upload-pack', ...commonArgs] : commonArgs; | |||||
return spawn(repoBasePath, command, args); | |||||
} | } | ||||
async delete(repoId: models.Repo['id'], subject: User): Promise<void> { | |||||
const repoIdString = repoId.toString(); | |||||
const repo = await this.prismaClient.repo.findUnique({ | |||||
where: { | |||||
id: repoId, | |||||
}, | |||||
rejectOnNotFound: notFoundFactory(models.RepoNotFoundError), | |||||
}); | |||||
await this.prismaClient.repo.delete({ | |||||
where: { | |||||
id: repoId, | |||||
}, | |||||
}); | |||||
const repoBasePath = GitServiceImpl.getRepoBasePath(repoIdString); | |||||
await unlink(repoBasePath); | |||||
async receivePack(repoBasePath: string): Promise<ChildProcess> { | |||||
const command = GitServiceImpl.isWindows() ? 'git' : 'git-receive-pack'; | |||||
const commonArgs = ['--stateless-rpc', '.']; | |||||
const args = GitServiceImpl.isWindows() ? ['receive-pack', ...commonArgs] : commonArgs; | |||||
return spawn(repoBasePath, command, args); | |||||
} | |||||
await this.logService.create( | |||||
subject, | |||||
models.GitAction.REPO_REMOVED, | |||||
{ | |||||
key: 'repoId', | |||||
value: repoIdString, | |||||
}, | |||||
{ | |||||
key: 'repoName', | |||||
value: repo.name, | |||||
}, | |||||
); | |||||
async uploadPack(repoBasePath: string): Promise<ChildProcess> { | |||||
const command = GitServiceImpl.isWindows() ? 'git' : 'git-upload-pack'; | |||||
const commonArgs = ['--stateless-rpc', '.']; | |||||
const args = GitServiceImpl.isWindows() ? ['upload-pack', ...commonArgs] : commonArgs; | |||||
return spawn(repoBasePath, command, args); | |||||
} | } | ||||
} | } |
@@ -1,2 +1 @@ | |||||
export * from './Git.service' | export * from './Git.service' | ||||
export * from './models' |
@@ -1,38 +0,0 @@ | |||||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||||
import {Org, User} from '../common'; | |||||
export enum RepoVisibility { | |||||
PRIVATE, | |||||
INTERNAL, | |||||
PUBLIC, | |||||
} | |||||
export enum OwnerType { | |||||
USER, | |||||
ORG, | |||||
} | |||||
export type Owner = User | Org; | |||||
export enum GitAction { | |||||
REPO_CREATED = 'REPO_CREATED', | |||||
REPO_REMOVED = 'REPO_REMOVED', | |||||
} | |||||
export type Repo = { | |||||
id: Uuid, | |||||
name: string, | |||||
ownerId: Uuid, | |||||
ownerType: OwnerType, | |||||
visibility: RepoVisibility, | |||||
} | |||||
export type CreateRepoData = Omit<Repo, 'id'>; | |||||
export class UnknownOwnerTypeError extends Error {} | |||||
export class UserNotFoundError extends Error {} | |||||
export class OrgNotFoundError extends Error {} | |||||
export class RepoNotFoundError extends Error {} |
@@ -1,33 +0,0 @@ | |||||
import { Prisma, PrismaClient } from '@prisma/client'; | |||||
import {User} from '../common'; | |||||
import {Log, LogParameterData} from './models'; | |||||
export interface LogService { | |||||
create(subject: User, action: string, ...parameters: LogParameterData[]): Promise<Log> | |||||
} | |||||
export class LogServiceImpl implements LogService { | |||||
private readonly prismaClient: PrismaClient | |||||
constructor() { | |||||
this.prismaClient = new PrismaClient() | |||||
} | |||||
async create(subject: User, action: string, ...parameters: LogParameterData[]): Promise<Log> { | |||||
const createData: Prisma.LogCreateInput = { | |||||
subjectUsername: subject.username, | |||||
subjectUserId: subject.id, | |||||
action, | |||||
} | |||||
if (parameters.length > 0) { | |||||
createData['parameters'] = { | |||||
create: parameters, | |||||
} | |||||
} | |||||
return this.prismaClient.log.create({ | |||||
data: createData, | |||||
}) | |||||
} | |||||
} |
@@ -1,2 +0,0 @@ | |||||
export * from './Log.service' | |||||
export * from './models' |
@@ -1,18 +0,0 @@ | |||||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||||
export type LogParameter = { | |||||
id: number, | |||||
logId: number, | |||||
key: string, | |||||
value: string, | |||||
} | |||||
export type Log = { | |||||
id: number, | |||||
subjectUserId: Uuid, | |||||
subjectUsername: string, | |||||
action: string, | |||||
createdAt: Date, | |||||
} | |||||
export type LogParameterData = Pick<LogParameter, 'key' | 'value'>; |
@@ -1 +0,0 @@ | |||||
export const notFoundFactory = <T extends Error>(ErrorClass: { new(): T }) => () => new ErrorClass() |
@@ -4,31 +4,4 @@ export const spawn = ( | |||||
cwd: string, | cwd: string, | ||||
command: string, | command: string, | ||||
args: string[], | args: string[], | ||||
parentProcess = process, | |||||
) => new Promise<string>((resolve, reject) => { | |||||
let stdout = ''; | |||||
let stderr = ''; | |||||
const theChildProcess = childProcess.spawn(command, args, { | |||||
cwd, | |||||
}); | |||||
theChildProcess.stdout.on('data', (data) => { | |||||
parentProcess.stdout.write(data); | |||||
stdout += data; | |||||
}); | |||||
theChildProcess.stderr.on('data', (data) => { | |||||
parentProcess.stderr.write(data); | |||||
stderr += data; | |||||
}); | |||||
theChildProcess.on('close', (code) => { | |||||
if (code !== 0) { | |||||
reject(new Error(stderr)); | |||||
return; | |||||
} | |||||
resolve(stdout); | |||||
}) | |||||
}) | |||||
) => childProcess.spawn(command, args, { cwd }); |
@@ -347,23 +347,6 @@ | |||||
resolved "https://registry.yarnpkg.com/@ovyerus/licenses/-/licenses-6.4.4.tgz#596e3ace46ab7c70bcf0e2b17f259796a4bedf9f" | resolved "https://registry.yarnpkg.com/@ovyerus/licenses/-/licenses-6.4.4.tgz#596e3ace46ab7c70bcf0e2b17f259796a4bedf9f" | ||||
integrity sha512-IHjc31WXciQT3hfvdY+M59jBkQp70Fpr04tNDVO5rez2PNv4u8tE6w//CkU+GeBoO9k2ahneSqzjzvlgjyjkGw== | integrity sha512-IHjc31WXciQT3hfvdY+M59jBkQp70Fpr04tNDVO5rez2PNv4u8tE6w//CkU+GeBoO9k2ahneSqzjzvlgjyjkGw== | ||||
"@prisma/client@^3.14.0": | |||||
version "3.14.0" | |||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.14.0.tgz#bb90405c012fcca11f4647d91153ed4c58f3bd48" | |||||
integrity sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA== | |||||
dependencies: | |||||
"@prisma/engines-version" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" | |||||
"@prisma/engines-version@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": | |||||
version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" | |||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#4edae57cf6527f35e22cebe75e49214fc0e99ac9" | |||||
integrity sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ== | |||||
"@prisma/engines@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": | |||||
version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" | |||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#7fa11bc26a51d450185c816cc0ab8cac673fb4bf" | |||||
integrity sha512-LwZvI3FY6f43xFjQNRuE10JM5R8vJzFTSmbV9X0Wuhv9kscLkjRlZt0BEoiHmO+2HA3B3xxbMfB5du7ZoSFXGg== | |||||
"@theoryofnekomata/uuid-buffer@^0.1.0": | "@theoryofnekomata/uuid-buffer@^0.1.0": | ||||
version "0.1.0" | version "0.1.0" | ||||
resolved "https://js.pack.modal.sh/@theoryofnekomata%2fuuid-buffer/-/uuid-buffer-0.1.0.tgz#0917314e8230ce1a2047172b3277512bc0fd73a3" | resolved "https://js.pack.modal.sh/@theoryofnekomata%2fuuid-buffer/-/uuid-buffer-0.1.0.tgz#0917314e8230ce1a2047172b3277512bc0fd73a3" | ||||
@@ -2435,13 +2418,6 @@ pridepack@1.1.1: | |||||
resolve.exports "^1.1.0" | resolve.exports "^1.1.0" | ||||
yargs "^17.2.1" | yargs "^17.2.1" | ||||
prisma@^3.14.0: | |||||
version "3.14.0" | |||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.14.0.tgz#dd67ece37d7b5373e9fd9588971de0024b49be81" | |||||
integrity sha512-l9MOgNCn/paDE+i1K2fp9NZ+Du4trzPTJsGkaQHVBufTGqzoYHuNk8JfzXuIn0Gte6/ZjyKj652Jq/Lc1tp2yw== | |||||
dependencies: | |||||
"@prisma/engines" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" | |||||
prompts@^2.3.2, prompts@^2.4.2: | prompts@^2.3.2, prompts@^2.4.2: | ||||
version "2.4.2" | version "2.4.2" | ||||
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" | resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" | ||||