@foundation0/git 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{packages/git/mcp → mcp}/cli.mjs +0 -0
- package/package.json +43 -13
- package/.codex.example/config.toml +0 -10
- package/.env.example +0 -10
- package/packages/fs/README.md +0 -47
- package/packages/fs/node_modules/.bin/f0-git-mcp +0 -21
- package/packages/fs/node_modules/.bin/f0-git-mcp-server +0 -21
- package/packages/fs/node_modules/.bin/f0-git-mcp-server.CMD +0 -12
- package/packages/fs/node_modules/.bin/f0-git-mcp-server.ps1 +0 -41
- package/packages/fs/node_modules/.bin/f0-git-mcp.CMD +0 -12
- package/packages/fs/node_modules/.bin/f0-git-mcp.ps1 +0 -41
- package/packages/fs/node_modules/.bin/tsc +0 -21
- package/packages/fs/node_modules/.bin/tsc.CMD +0 -12
- package/packages/fs/node_modules/.bin/tsc.ps1 +0 -41
- package/packages/fs/node_modules/.bin/tsserver +0 -21
- package/packages/fs/node_modules/.bin/tsserver.CMD +0 -12
- package/packages/fs/node_modules/.bin/tsserver.ps1 +0 -41
- package/packages/fs/node_modules/.bin/vite +0 -21
- package/packages/fs/node_modules/.bin/vite.CMD +0 -12
- package/packages/fs/node_modules/.bin/vite.ps1 +0 -41
- package/packages/fs/node_modules/.bin/vitest +0 -21
- package/packages/fs/node_modules/.bin/vitest.CMD +0 -12
- package/packages/fs/node_modules/.bin/vitest.ps1 +0 -41
- package/packages/fs/package.json +0 -28
- package/packages/fs/src/cli.ts +0 -74
- package/packages/fs/src/git-fs.ts +0 -705
- package/packages/fs/src/index.ts +0 -33
- package/packages/fs/src/mount.ts +0 -297
- package/packages/fs/tsconfig.json +0 -7
- package/packages/git/mcp/tests/e2e/git-mcp-e2e.spec.ts +0 -157
- package/packages/git/mcp/tests/e2e/server.fixture.ts +0 -109
- package/packages/git/node_modules/.bin/tsc +0 -21
- package/packages/git/node_modules/.bin/tsc.CMD +0 -12
- package/packages/git/node_modules/.bin/tsc.ps1 +0 -41
- package/packages/git/node_modules/.bin/tsserver +0 -21
- package/packages/git/node_modules/.bin/tsserver.CMD +0 -12
- package/packages/git/node_modules/.bin/tsserver.ps1 +0 -41
- package/packages/git/node_modules/.bin/vite +0 -21
- package/packages/git/node_modules/.bin/vite.CMD +0 -12
- package/packages/git/node_modules/.bin/vite.ps1 +0 -41
- package/packages/git/node_modules/.bin/vitest +0 -21
- package/packages/git/node_modules/.bin/vitest.CMD +0 -12
- package/packages/git/node_modules/.bin/vitest.ps1 +0 -41
- package/packages/git/node_modules/.vite/vitest/results.json +0 -1
- package/packages/git/package.json +0 -60
- package/packages/git/scripts/create-issue.mjs +0 -93
- package/packages/git/scripts/extract-git-spec.mjs +0 -234
- package/packages/git/scripts/fetch-gitea-swagger.mjs +0 -22
- package/packages/git/tests/api.spec.ts +0 -55
- package/packages/git/tests/e2e/git-service-feature-e2e.spec.ts +0 -232
- package/packages/git/tests/git-service-api-object.spec.ts +0 -97
- package/packages/git/tests/git-service-feature-matrix.spec.ts +0 -182
- package/packages/git/tests/issue-dependencies.spec.ts +0 -81
- package/packages/git/tsconfig.json +0 -7
- package/packages/git/vitest.config.ts +0 -7
- package/packages/utils/package.json +0 -9
- package/packages/utils/src/awk.ts +0 -6
- package/packages/utils/src/cat.ts +0 -6
- package/packages/utils/src/cd.ts +0 -6
- package/packages/utils/src/chgrp.ts +0 -6
- package/packages/utils/src/chmod.ts +0 -6
- package/packages/utils/src/chown.ts +0 -6
- package/packages/utils/src/cp.ts +0 -6
- package/packages/utils/src/curl.ts +0 -6
- package/packages/utils/src/cut.ts +0 -6
- package/packages/utils/src/date.ts +0 -6
- package/packages/utils/src/echo.ts +0 -6
- package/packages/utils/src/find.ts +0 -6
- package/packages/utils/src/grep.ts +0 -6
- package/packages/utils/src/gunzip.ts +0 -6
- package/packages/utils/src/gzip.ts +0 -6
- package/packages/utils/src/head.ts +0 -6
- package/packages/utils/src/hostname.ts +0 -6
- package/packages/utils/src/index.ts +0 -37
- package/packages/utils/src/ls.ts +0 -6
- package/packages/utils/src/mkdir.ts +0 -6
- package/packages/utils/src/mv.ts +0 -6
- package/packages/utils/src/ping.ts +0 -6
- package/packages/utils/src/pwd.ts +0 -6
- package/packages/utils/src/rm.ts +0 -6
- package/packages/utils/src/rmdir.ts +0 -6
- package/packages/utils/src/sed.ts +0 -6
- package/packages/utils/src/sort.ts +0 -6
- package/packages/utils/src/tail.ts +0 -6
- package/packages/utils/src/tar.ts +0 -6
- package/packages/utils/src/touch.ts +0 -6
- package/packages/utils/src/tr.ts +0 -6
- package/packages/utils/src/uname.ts +0 -6
- package/packages/utils/src/uniq.ts +0 -6
- package/packages/utils/src/unzip.ts +0 -6
- package/packages/utils/src/util.ts +0 -4
- package/packages/utils/src/wc.ts +0 -6
- package/packages/utils/src/wget.ts +0 -6
- package/packages/utils/src/whoami.ts +0 -6
- package/packages/utils/src/zip.ts +0 -6
- package/pnpm-workspace.yaml +0 -2
- package/tsconfig.base.json +0 -12
- /package/{packages/git/README.md → README.md} +0 -0
- /package/{packages/git/gitea-swagger.json → gitea-swagger.json} +0 -0
- /package/{packages/git/mcp → mcp}/README.md +0 -0
- /package/{packages/git/mcp → mcp}/src/cli.ts +0 -0
- /package/{packages/git/mcp → mcp}/src/client.ts +0 -0
- /package/{packages/git/mcp → mcp}/src/index.ts +0 -0
- /package/{packages/git/mcp → mcp}/src/server.ts +0 -0
- /package/{packages/git/src → src}/api.ts +0 -0
- /package/{packages/git/src → src}/git-service-api.ts +0 -0
- /package/{packages/git/src → src}/git-service-feature-spec.generated.ts +0 -0
- /package/{packages/git/src → src}/index.ts +0 -0
- /package/{packages/git/src → src}/issue-dependencies.ts +0 -0
- /package/{packages/git/src → src}/platform/config.ts +0 -0
- /package/{packages/git/src → src}/platform/gitea-adapter.ts +0 -0
- /package/{packages/git/src → src}/platform/gitea-rules.ts +0 -0
- /package/{packages/git/src → src}/platform/index.ts +0 -0
- /package/{packages/git/src → src}/repository.ts +0 -0
- /package/{packages/git/src → src}/spec-mock.ts +0 -0
|
@@ -1,705 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import {
|
|
3
|
-
type GitServiceApi,
|
|
4
|
-
type GitServiceApiExecutionResult,
|
|
5
|
-
type GitServiceApiFactoryOptions,
|
|
6
|
-
type GitServiceApiMethod,
|
|
7
|
-
createGitServiceApi,
|
|
8
|
-
} from '@foundation0/git'
|
|
9
|
-
|
|
10
|
-
type GitFsQuery = Record<string, string | number | boolean>
|
|
11
|
-
type WriteData = string | Uint8Array | ArrayBuffer
|
|
12
|
-
type FileKind = 'file' | 'directory'
|
|
13
|
-
|
|
14
|
-
export type GitFsOptions = Omit<GitServiceApiFactoryOptions, 'defaultOwner' | 'defaultRepo' | 'config'> & {
|
|
15
|
-
api?: GitServiceApi
|
|
16
|
-
config?: GitServiceApiFactoryOptions['config']
|
|
17
|
-
defaultOwner?: string
|
|
18
|
-
defaultRepo?: string
|
|
19
|
-
defaultCommitMessage?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type ReadFileOptions = {
|
|
23
|
-
encoding?: BufferEncoding
|
|
24
|
-
ref?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type WriteFileOptions = {
|
|
28
|
-
encoding?: BufferEncoding | 'base64'
|
|
29
|
-
message?: string
|
|
30
|
-
branch?: string
|
|
31
|
-
author?: unknown
|
|
32
|
-
committer?: unknown
|
|
33
|
-
flag?: string
|
|
34
|
-
overwrite?: boolean
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type ReaddirOptions = {
|
|
38
|
-
withFileTypes?: boolean
|
|
39
|
-
ref?: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type RmOptions = {
|
|
43
|
-
recursive?: boolean
|
|
44
|
-
force?: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type GitFsError = NodeJS.ErrnoException
|
|
48
|
-
|
|
49
|
-
export interface GitFsDirentLike {
|
|
50
|
-
name: string
|
|
51
|
-
isFile: () => boolean
|
|
52
|
-
isDirectory: () => boolean
|
|
53
|
-
isSymbolicLink: () => boolean
|
|
54
|
-
isBlockDevice: () => boolean
|
|
55
|
-
isCharacterDevice: () => boolean
|
|
56
|
-
isFIFO: () => boolean
|
|
57
|
-
isSocket: () => boolean
|
|
58
|
-
isUnknown: () => boolean
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface GitFsStats {
|
|
62
|
-
isFile: () => boolean
|
|
63
|
-
isDirectory: () => boolean
|
|
64
|
-
isSymbolicLink: () => boolean
|
|
65
|
-
isFIFO: () => boolean
|
|
66
|
-
isCharacterDevice: () => boolean
|
|
67
|
-
isBlockDevice: () => boolean
|
|
68
|
-
isSocket: () => boolean
|
|
69
|
-
isUnknown: () => boolean
|
|
70
|
-
dev: number
|
|
71
|
-
ino: number
|
|
72
|
-
mode: number
|
|
73
|
-
nlink: number
|
|
74
|
-
uid: number
|
|
75
|
-
gid: number
|
|
76
|
-
rdev: number
|
|
77
|
-
size: number
|
|
78
|
-
blksize: number
|
|
79
|
-
blocks: number
|
|
80
|
-
atimeMs: number
|
|
81
|
-
mtimeMs: number
|
|
82
|
-
ctimeMs: number
|
|
83
|
-
birthtimeMs: number
|
|
84
|
-
atime: Date
|
|
85
|
-
mtime: Date
|
|
86
|
-
ctime: Date
|
|
87
|
-
birthtime: Date
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
interface ParsedPath {
|
|
91
|
-
owner: string
|
|
92
|
-
repo: string
|
|
93
|
-
repoPath: string
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface GitContentEntry {
|
|
97
|
-
name: string
|
|
98
|
-
path: string
|
|
99
|
-
type?: 'file' | 'dir' | string
|
|
100
|
-
sha?: string
|
|
101
|
-
size?: number
|
|
102
|
-
content?: string
|
|
103
|
-
encoding?: string
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const createFsError = (code: string, message: string): GitFsError => {
|
|
107
|
-
const error = new Error(message) as GitFsError
|
|
108
|
-
error.code = code
|
|
109
|
-
return error
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const normalizePath = (value: string): string => value.replace(/\\/g, '/').trim()
|
|
113
|
-
|
|
114
|
-
const toSegments = (value: string): string[] =>
|
|
115
|
-
normalizePath(value)
|
|
116
|
-
.split('/')
|
|
117
|
-
.map((part) => part.trim())
|
|
118
|
-
.filter(Boolean)
|
|
119
|
-
|
|
120
|
-
const normalizeRepoPath = (repoPath: string): string => repoPath.replace(/^\/+|\/+$/g, '')
|
|
121
|
-
|
|
122
|
-
const parsePath = (
|
|
123
|
-
value: string,
|
|
124
|
-
defaults: { defaultOwner?: string; defaultRepo?: string },
|
|
125
|
-
): ParsedPath => {
|
|
126
|
-
const parts = toSegments(value)
|
|
127
|
-
|
|
128
|
-
if (parts.length === 0) {
|
|
129
|
-
if (!defaults.defaultOwner || !defaults.defaultRepo) {
|
|
130
|
-
throw createFsError('ENOENT', `Path requires a repository root or default owner/repo: ${value}`)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
owner: defaults.defaultOwner,
|
|
135
|
-
repo: defaults.defaultRepo,
|
|
136
|
-
repoPath: '',
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (parts.length === 1) {
|
|
141
|
-
if (!defaults.defaultOwner || !defaults.defaultRepo) {
|
|
142
|
-
throw createFsError('ENOENT', `Path requires owner/repo prefix: ${value}`)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
owner: defaults.defaultOwner,
|
|
147
|
-
repo: defaults.defaultRepo,
|
|
148
|
-
repoPath: normalizeRepoPath(parts[0]),
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
owner: parts[0],
|
|
154
|
-
repo: parts[1],
|
|
155
|
-
repoPath: normalizeRepoPath(parts.slice(2).join('/')),
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
160
|
-
typeof value === 'object' && value !== null
|
|
161
|
-
|
|
162
|
-
const parseEntries = (body: unknown): GitContentEntry[] => {
|
|
163
|
-
if (!Array.isArray(body)) {
|
|
164
|
-
return []
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return body
|
|
168
|
-
.map((entry): GitContentEntry | null => {
|
|
169
|
-
if (!isRecord(entry)) {
|
|
170
|
-
return null
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const name = typeof entry.name === 'string' ? entry.name : undefined
|
|
174
|
-
if (!name) {
|
|
175
|
-
return null
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
name,
|
|
180
|
-
path: typeof entry.path === 'string' ? entry.path : name,
|
|
181
|
-
type: typeof entry.type === 'string' ? entry.type : undefined,
|
|
182
|
-
sha: typeof entry.sha === 'string' ? entry.sha : undefined,
|
|
183
|
-
size: typeof entry.size === 'number' ? entry.size : undefined,
|
|
184
|
-
content: typeof entry.content === 'string' ? entry.content : undefined,
|
|
185
|
-
encoding: typeof entry.encoding === 'string' ? entry.encoding : undefined,
|
|
186
|
-
}
|
|
187
|
-
})
|
|
188
|
-
.filter((entry): entry is GitContentEntry => entry !== null)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const parseFileEntry = (body: unknown): GitContentEntry | null => {
|
|
192
|
-
if (!isRecord(body)) {
|
|
193
|
-
return null
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const name = typeof body.name === 'string'
|
|
197
|
-
? body.name
|
|
198
|
-
: typeof body.path === 'string'
|
|
199
|
-
? body.path
|
|
200
|
-
: ''
|
|
201
|
-
if (!name) {
|
|
202
|
-
return null
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
name,
|
|
207
|
-
path: typeof body.path === 'string' ? body.path : name,
|
|
208
|
-
type: typeof body.type === 'string' ? body.type : undefined,
|
|
209
|
-
sha: typeof body.sha === 'string' ? body.sha : undefined,
|
|
210
|
-
size: typeof body.size === 'number' ? body.size : undefined,
|
|
211
|
-
content: typeof body.content === 'string' ? body.content : undefined,
|
|
212
|
-
encoding: typeof body.encoding === 'string' ? body.encoding : undefined,
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const makeStats = (kind: FileKind, entry?: GitContentEntry): GitFsStats => {
|
|
217
|
-
const isDir = kind === 'directory'
|
|
218
|
-
const size = isDir ? 0 : (entry?.size ?? 0)
|
|
219
|
-
const timestamp = Date.now()
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
dev: 0,
|
|
223
|
-
ino: 0,
|
|
224
|
-
mode: isDir ? 0o040755 : 0o100644,
|
|
225
|
-
nlink: isDir ? 2 : 1,
|
|
226
|
-
uid: 0,
|
|
227
|
-
gid: 0,
|
|
228
|
-
rdev: 0,
|
|
229
|
-
size,
|
|
230
|
-
blksize: 4096,
|
|
231
|
-
blocks: Math.max(1, Math.ceil(size / 4096)),
|
|
232
|
-
atimeMs: timestamp,
|
|
233
|
-
mtimeMs: timestamp,
|
|
234
|
-
ctimeMs: timestamp,
|
|
235
|
-
birthtimeMs: timestamp,
|
|
236
|
-
atime: new Date(timestamp),
|
|
237
|
-
mtime: new Date(timestamp),
|
|
238
|
-
ctime: new Date(timestamp),
|
|
239
|
-
birthtime: new Date(timestamp),
|
|
240
|
-
isFile: () => !isDir,
|
|
241
|
-
isDirectory: () => isDir,
|
|
242
|
-
isSymbolicLink: () => false,
|
|
243
|
-
isFIFO: () => false,
|
|
244
|
-
isCharacterDevice: () => false,
|
|
245
|
-
isBlockDevice: () => false,
|
|
246
|
-
isSocket: () => false,
|
|
247
|
-
isUnknown: () => false,
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const makeDirent = (entry: GitContentEntry): GitFsDirentLike => {
|
|
252
|
-
const isDir = entry.type === 'dir'
|
|
253
|
-
return {
|
|
254
|
-
name: entry.name,
|
|
255
|
-
isFile: () => !isDir,
|
|
256
|
-
isDirectory: () => isDir,
|
|
257
|
-
isSymbolicLink: () => false,
|
|
258
|
-
isBlockDevice: () => false,
|
|
259
|
-
isCharacterDevice: () => false,
|
|
260
|
-
isFIFO: () => false,
|
|
261
|
-
isSocket: () => false,
|
|
262
|
-
isUnknown: () => false,
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const responseToError = (status: number, action: string, path: string): GitFsError => {
|
|
267
|
-
if (status === 404) {
|
|
268
|
-
return createFsError('ENOENT', `${action}: no such path ${path}`)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (status === 401 || status === 403) {
|
|
272
|
-
return createFsError('EACCES', `${action}: access denied for ${path}`)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (status === 409) {
|
|
276
|
-
return createFsError('EEXIST', `${action}: conflict ${path}`)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (status >= 500) {
|
|
280
|
-
return createFsError('EIO', `${action}: remote API failed for ${path} (${status})`)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return createFsError('EIO', `${action}: remote API error for ${path} (${status})`)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const toGitPath = (path: ParsedPath): string => `${path.owner}/${path.repo}/${path.repoPath}`
|
|
287
|
-
|
|
288
|
-
const encodeForGit = (value: WriteData, encoding: BufferEncoding | 'base64' | undefined): string => {
|
|
289
|
-
if (typeof value === 'string') {
|
|
290
|
-
return Buffer.from(value, encoding === 'base64' ? 'base64' : encoding ?? 'utf8').toString('base64')
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (value instanceof ArrayBuffer) {
|
|
294
|
-
return Buffer.from(value).toString('base64')
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return Buffer.from(value).toString('base64')
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const decodeFromGit = (entry: GitContentEntry): Buffer => {
|
|
301
|
-
if (typeof entry.content === 'string') {
|
|
302
|
-
return Buffer.from(entry.content.replace(/\s+/g, ''), 'base64')
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return Buffer.from([])
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const commitMessage = (operation: string, gitPath: string, explicit?: string, defaults?: string): string => {
|
|
309
|
-
if (explicit) {
|
|
310
|
-
return explicit
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (defaults) {
|
|
314
|
-
return defaults
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return `${operation} ${gitPath} [${randomUUID().slice(0, 6)}]`
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
interface GitFsAsyncApi {
|
|
321
|
-
rmdir(path: string): Promise<void>
|
|
322
|
-
readdir(path: string, options?: ReaddirOptions): Promise<string[] | GitFsDirentLike[]>
|
|
323
|
-
readFile(path: string, options?: ReadFileOptions): Promise<Buffer | string>
|
|
324
|
-
writeFile(path: string, data: WriteData, options?: WriteFileOptions): Promise<void>
|
|
325
|
-
appendFile(path: string, data: WriteData, options?: WriteFileOptions): Promise<void>
|
|
326
|
-
mkdir(path: string): Promise<void>
|
|
327
|
-
rm(path: string, options?: RmOptions): Promise<void>
|
|
328
|
-
unlink(path: string): Promise<void>
|
|
329
|
-
rename(oldPath: string, newPath: string): Promise<void>
|
|
330
|
-
copyFile(src: string, dest: string): Promise<void>
|
|
331
|
-
stat(path: string): Promise<GitFsStats>
|
|
332
|
-
lstat(path: string): Promise<GitFsStats>
|
|
333
|
-
exists(path: string): Promise<boolean>
|
|
334
|
-
access(path: string): Promise<void>
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export interface GitFsInstance extends GitFsAsyncApi {
|
|
338
|
-
api: GitServiceApi
|
|
339
|
-
promises: GitFsAsyncApi
|
|
340
|
-
rmdirSync(path: string): never
|
|
341
|
-
readFileSync(path: string): never
|
|
342
|
-
writeFileSync(path: string): never
|
|
343
|
-
appendFileSync(path: string): never
|
|
344
|
-
readdirSync(path: string): never
|
|
345
|
-
mkdirSync(path: string): never
|
|
346
|
-
rmSync(path: string): never
|
|
347
|
-
unlinkSync(path: string): never
|
|
348
|
-
renameSync(oldPath: string, newPath: string): never
|
|
349
|
-
copyFileSync(src: string, dest: string): never
|
|
350
|
-
statSync(path: string): never
|
|
351
|
-
lstatSync(path: string): never
|
|
352
|
-
existsSync(path: string): never
|
|
353
|
-
accessSync(path: string): never
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const buildGitApi = (options: GitFsOptions): GitServiceApi => {
|
|
357
|
-
if (options.api) {
|
|
358
|
-
return options.api
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return createGitServiceApi({
|
|
362
|
-
config: options.config,
|
|
363
|
-
defaultOwner: options.defaultOwner,
|
|
364
|
-
defaultRepo: options.defaultRepo,
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const unsupportedSync = (): never => {
|
|
369
|
-
throw createFsError('ENOSYS', 'Git filesystem sync APIs are not supported')
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export const createGitFs = (options: GitFsOptions = {}): GitFsInstance => {
|
|
373
|
-
const api = buildGitApi(options)
|
|
374
|
-
const defaults = { defaultOwner: options.defaultOwner, defaultRepo: options.defaultRepo }
|
|
375
|
-
const defaultMessage = options.defaultCommitMessage
|
|
376
|
-
|
|
377
|
-
const callContentsMethod = async (
|
|
378
|
-
action: 'list' | 'view' | 'create' | 'update' | 'delete',
|
|
379
|
-
target: ParsedPath,
|
|
380
|
-
query?: GitFsQuery,
|
|
381
|
-
payload?: Record<string, unknown>,
|
|
382
|
-
): Promise<GitServiceApiExecutionResult> => {
|
|
383
|
-
const actionMethod = api.repo.contents[action] as GitServiceApiMethod
|
|
384
|
-
const args: unknown[] = [target.owner, target.repo, target.repoPath || '']
|
|
385
|
-
|
|
386
|
-
const request: Record<string, unknown> = {}
|
|
387
|
-
if (query && Object.keys(query).length > 0) {
|
|
388
|
-
request.query = query
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (payload !== undefined) {
|
|
392
|
-
request.data = payload
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (Object.keys(request).length > 0) {
|
|
396
|
-
args.push(request)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const response = await actionMethod(...args)
|
|
400
|
-
if (!response.ok) {
|
|
401
|
-
throw responseToError(response.status, action, toGitPath(target))
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return response
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const listEntries = async (target: ParsedPath, query?: GitFsQuery): Promise<GitContentEntry[]> => {
|
|
408
|
-
const response = await callContentsMethod('list', target, query)
|
|
409
|
-
return parseEntries(response.body)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const readEntry = async (
|
|
413
|
-
target: ParsedPath,
|
|
414
|
-
query?: GitFsQuery,
|
|
415
|
-
): Promise<GitContentEntry | null> => {
|
|
416
|
-
const response = await callContentsMethod('view', target, query)
|
|
417
|
-
if (Array.isArray(response.body)) {
|
|
418
|
-
return null
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return parseFileEntry(response.body)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const resolveRefQuery = (options?: { ref?: string }): GitFsQuery | undefined => {
|
|
425
|
-
if (!options?.ref) {
|
|
426
|
-
return undefined
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return { ref: options.ref }
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const rmdir: GitFsAsyncApi['rmdir'] = async () => {
|
|
433
|
-
throw createFsError('ENOSYS', 'rmdir is not supported for Git-backed contents')
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const readdir: GitFsAsyncApi['readdir'] = async (path, options) => {
|
|
437
|
-
const target = parsePath(path, defaults)
|
|
438
|
-
const entries = await listEntries(target, resolveRefQuery(options))
|
|
439
|
-
const filtered = entries.filter((entry) => entry.name.length > 0)
|
|
440
|
-
|
|
441
|
-
if (options?.withFileTypes) {
|
|
442
|
-
return filtered.map((entry) => makeDirent(entry))
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return filtered.map((entry) => entry.name)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const readFile: GitFsAsyncApi['readFile'] = async (path, options) => {
|
|
449
|
-
const target = parsePath(path, defaults)
|
|
450
|
-
const response = await callContentsMethod('view', target, resolveRefQuery(options))
|
|
451
|
-
if (Array.isArray(response.body)) {
|
|
452
|
-
throw createFsError('EISDIR', `Path is a directory: ${toGitPath(target)}`)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const entry = parseFileEntry(response.body)
|
|
456
|
-
if (!entry) {
|
|
457
|
-
throw createFsError('ENOENT', `No such file: ${toGitPath(target)}`)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (entry.type === 'dir') {
|
|
461
|
-
throw createFsError('EISDIR', `Path is a directory: ${toGitPath(target)}`)
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const data = decodeFromGit(entry)
|
|
465
|
-
if (options?.encoding) {
|
|
466
|
-
return data.toString(options.encoding)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
return data
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const writeFile: GitFsAsyncApi['writeFile'] = async (path, data, options = {}) => {
|
|
473
|
-
const target = parsePath(path, defaults)
|
|
474
|
-
const existing = await readEntry(target).catch((error) => {
|
|
475
|
-
if ((error as GitFsError).code === 'ENOENT') {
|
|
476
|
-
return null
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
throw error
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
if (existing?.type === 'dir') {
|
|
483
|
-
throw createFsError('EISDIR', `Cannot write a file over directory: ${toGitPath(target)}`)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (existing && options.overwrite === false) {
|
|
487
|
-
throw createFsError('EEXIST', `File exists: ${toGitPath(target)}`)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const payload = {
|
|
491
|
-
content: encodeForGit(data, options.encoding),
|
|
492
|
-
message: commitMessage(
|
|
493
|
-
existing ? 'overwrite' : 'file',
|
|
494
|
-
toGitPath(target),
|
|
495
|
-
options.message,
|
|
496
|
-
defaultMessage,
|
|
497
|
-
),
|
|
498
|
-
sha: existing?.sha,
|
|
499
|
-
branch: options.branch,
|
|
500
|
-
author: options.author,
|
|
501
|
-
committer: options.committer,
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (existing) {
|
|
505
|
-
if (!payload.sha) {
|
|
506
|
-
throw createFsError('EINVAL', `No sha available for update: ${toGitPath(target)}`)
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
await callContentsMethod('update', target, undefined, payload)
|
|
510
|
-
return
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
await callContentsMethod('create', target, undefined, payload)
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const appendFile: GitFsAsyncApi['appendFile'] = async (path, data, options) => {
|
|
517
|
-
const current = await readFile(path).catch((error) => {
|
|
518
|
-
if ((error as GitFsError).code === 'ENOENT') {
|
|
519
|
-
return Buffer.from([])
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
throw error
|
|
523
|
-
})
|
|
524
|
-
const base = typeof current === 'string' ? Buffer.from(current) : current
|
|
525
|
-
const payload = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data)
|
|
526
|
-
await writeFile(path, Buffer.concat([base, payload]), options)
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const mkdir: GitFsAsyncApi['mkdir'] = async () => {
|
|
530
|
-
throw createFsError('ENOSYS', 'mkdir is not supported by Git contents API')
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const rm: GitFsAsyncApi['rm'] = async (path, options = {}) => {
|
|
534
|
-
if (options.recursive) {
|
|
535
|
-
throw createFsError('ENOSYS', 'recursive rm is not supported for Git-backed FS')
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
await unlink(path)
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const unlink: GitFsAsyncApi['unlink'] = async (path) => {
|
|
542
|
-
const target = parsePath(path, defaults)
|
|
543
|
-
const existing = await readEntry(target)
|
|
544
|
-
if (!existing) {
|
|
545
|
-
throw createFsError('ENOENT', `No such file: ${toGitPath(target)}`)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (existing.type === 'dir') {
|
|
549
|
-
throw createFsError('EISDIR', `Cannot unlink directory: ${toGitPath(target)}`)
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
if (!existing.sha) {
|
|
553
|
-
throw createFsError('EINVAL', `No sha available for delete: ${toGitPath(target)}`)
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
await callContentsMethod('delete', target, undefined, {
|
|
557
|
-
message: commitMessage('delete', toGitPath(target), undefined, defaultMessage),
|
|
558
|
-
sha: existing.sha,
|
|
559
|
-
})
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const rename: GitFsAsyncApi['rename'] = async (oldPath, newPath) => {
|
|
563
|
-
const content = await readFile(oldPath)
|
|
564
|
-
await writeFile(newPath, typeof content === 'string' ? content : Buffer.from(content))
|
|
565
|
-
await unlink(oldPath)
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const copyFile: GitFsAsyncApi['copyFile'] = async (src, dest) => {
|
|
569
|
-
const content = await readFile(src)
|
|
570
|
-
await writeFile(dest, typeof content === 'string' ? content : Buffer.from(content))
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const stat: GitFsAsyncApi['stat'] = async (path) => {
|
|
574
|
-
const target = parsePath(path, defaults)
|
|
575
|
-
|
|
576
|
-
if (target.repoPath === '') {
|
|
577
|
-
return makeStats('directory')
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
try {
|
|
581
|
-
await listEntries(target)
|
|
582
|
-
return makeStats('directory')
|
|
583
|
-
} catch (error) {
|
|
584
|
-
const code = (error as GitFsError).code
|
|
585
|
-
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
586
|
-
throw error
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const existing = await readEntry(target)
|
|
591
|
-
if (!existing) {
|
|
592
|
-
throw createFsError('ENOENT', `No such path: ${toGitPath(target)}`)
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (existing.type === 'dir') {
|
|
596
|
-
return makeStats('directory', existing)
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
return makeStats('file', existing)
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const lstat: GitFsAsyncApi['lstat'] = async (path) => {
|
|
603
|
-
return stat(path)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const existsAsync: GitFsAsyncApi['exists'] = async (path) => {
|
|
607
|
-
try {
|
|
608
|
-
const target = parsePath(path, defaults)
|
|
609
|
-
const entry = await readEntry(target)
|
|
610
|
-
return Boolean(entry)
|
|
611
|
-
} catch (error) {
|
|
612
|
-
if ((error as GitFsError).code === 'ENOENT') {
|
|
613
|
-
return false
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
throw error
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const access: GitFsAsyncApi['access'] = async (path) => {
|
|
621
|
-
const found = await existsAsync(path)
|
|
622
|
-
if (!found) {
|
|
623
|
-
throw createFsError('ENOENT', `No such path: ${path}`)
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const asyncApi: GitFsAsyncApi = {
|
|
628
|
-
rmdir,
|
|
629
|
-
readdir,
|
|
630
|
-
readFile,
|
|
631
|
-
writeFile,
|
|
632
|
-
appendFile,
|
|
633
|
-
mkdir,
|
|
634
|
-
rm,
|
|
635
|
-
unlink,
|
|
636
|
-
rename,
|
|
637
|
-
copyFile,
|
|
638
|
-
stat,
|
|
639
|
-
lstat,
|
|
640
|
-
exists: existsAsync,
|
|
641
|
-
access,
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const syncUnsupported = {
|
|
645
|
-
rmdirSync: unsupportedSync,
|
|
646
|
-
readFileSync: unsupportedSync,
|
|
647
|
-
writeFileSync: unsupportedSync,
|
|
648
|
-
appendFileSync: unsupportedSync,
|
|
649
|
-
readdirSync: unsupportedSync,
|
|
650
|
-
mkdirSync: unsupportedSync,
|
|
651
|
-
rmSync: unsupportedSync,
|
|
652
|
-
unlinkSync: unsupportedSync,
|
|
653
|
-
renameSync: unsupportedSync,
|
|
654
|
-
copyFileSync: unsupportedSync,
|
|
655
|
-
statSync: unsupportedSync,
|
|
656
|
-
lstatSync: unsupportedSync,
|
|
657
|
-
existsSync: unsupportedSync,
|
|
658
|
-
accessSync: unsupportedSync,
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
return {
|
|
662
|
-
api,
|
|
663
|
-
...asyncApi,
|
|
664
|
-
...syncUnsupported,
|
|
665
|
-
promises: asyncApi,
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
export const defaultGitFs: GitFsInstance = createGitFs({
|
|
670
|
-
config: {
|
|
671
|
-
platform: 'GITEA',
|
|
672
|
-
},
|
|
673
|
-
defaultOwner: process.env.GITEA_TEST_OWNER ?? 'example-org',
|
|
674
|
-
defaultRepo: process.env.GITEA_TEST_REPO ?? 'example-repo',
|
|
675
|
-
})
|
|
676
|
-
|
|
677
|
-
export const promises = defaultGitFs.promises
|
|
678
|
-
export const readdir = defaultGitFs.readdir
|
|
679
|
-
export const readFile = defaultGitFs.readFile
|
|
680
|
-
export const writeFile = defaultGitFs.writeFile
|
|
681
|
-
export const appendFile = defaultGitFs.appendFile
|
|
682
|
-
export const mkdir = defaultGitFs.mkdir
|
|
683
|
-
export const rm = defaultGitFs.rm
|
|
684
|
-
export const unlink = defaultGitFs.unlink
|
|
685
|
-
export const rename = defaultGitFs.rename
|
|
686
|
-
export const copyFile = defaultGitFs.copyFile
|
|
687
|
-
export const stat = defaultGitFs.stat
|
|
688
|
-
export const lstat = defaultGitFs.lstat
|
|
689
|
-
export const exists = defaultGitFs.exists
|
|
690
|
-
export const access = defaultGitFs.access
|
|
691
|
-
export const rmdir = defaultGitFs.rmdir
|
|
692
|
-
export const readFileSync = defaultGitFs.readFileSync
|
|
693
|
-
export const writeFileSync = defaultGitFs.writeFileSync
|
|
694
|
-
export const appendFileSync = defaultGitFs.appendFileSync
|
|
695
|
-
export const readdirSync = defaultGitFs.readdirSync
|
|
696
|
-
export const mkdirSync = defaultGitFs.mkdirSync
|
|
697
|
-
export const rmSync = defaultGitFs.rmSync
|
|
698
|
-
export const rmdirSync = defaultGitFs.rmdirSync
|
|
699
|
-
export const unlinkSync = defaultGitFs.unlinkSync
|
|
700
|
-
export const renameSync = defaultGitFs.renameSync
|
|
701
|
-
export const copyFileSync = defaultGitFs.copyFileSync
|
|
702
|
-
export const statSync = defaultGitFs.statSync
|
|
703
|
-
export const lstatSync = defaultGitFs.lstatSync
|
|
704
|
-
export const existsSync = defaultGitFs.existsSync
|
|
705
|
-
export const accessSync = defaultGitFs.accessSync
|