@foundation0/git 1.3.0 → 1.3.2
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/README.md +291 -291
- package/gitea-swagger.json +28627 -28627
- package/mcp/README.md +262 -247
- package/mcp/cli.mjs +37 -37
- package/mcp/src/cli.ts +76 -76
- package/mcp/src/client.ts +143 -134
- package/mcp/src/index.ts +7 -7
- package/mcp/src/redaction.ts +207 -207
- package/mcp/src/server.ts +2313 -814
- package/package.json +3 -1
- package/src/actions-api.ts +860 -637
- package/src/api.ts +69 -69
- package/src/ci-api.ts +544 -544
- package/src/git-service-api.ts +822 -754
- package/src/git-service-feature-spec.generated.ts +5341 -5341
- package/src/index.ts +55 -55
- package/src/issue-dependencies.ts +533 -533
- package/src/label-management.ts +587 -587
- package/src/platform/config.ts +62 -62
- package/src/platform/gitea-adapter.ts +460 -460
- package/src/platform/gitea-rules.ts +129 -129
- package/src/platform/index.ts +44 -44
- package/src/repository.ts +151 -151
- package/src/spec-mock.ts +45 -45
package/src/repository.ts
CHANGED
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
import fsSync from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
export interface GitRepositoryIdentity {
|
|
5
|
-
owner: string
|
|
6
|
-
repo: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function parseGitRemoteUrl(remoteUrl: string): GitRepositoryIdentity | null {
|
|
10
|
-
try {
|
|
11
|
-
if (remoteUrl.startsWith('http://') || remoteUrl.startsWith('https://') || remoteUrl.startsWith('ssh://')) {
|
|
12
|
-
const parsed = new URL(remoteUrl)
|
|
13
|
-
const segments = parsed.pathname.split('/').filter(Boolean)
|
|
14
|
-
if (segments.length >= 2) {
|
|
15
|
-
const owner = segments[segments.length - 2]
|
|
16
|
-
const rawRepo = segments[segments.length - 1]
|
|
17
|
-
const repo = rawRepo.replace(/\.git$/, '')
|
|
18
|
-
return { owner, repo }
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (remoteUrl.startsWith('git@')) {
|
|
23
|
-
const parts = remoteUrl.split(':', 2)
|
|
24
|
-
if (parts.length === 2) {
|
|
25
|
-
const pathParts = parts[1].split('/').filter(Boolean)
|
|
26
|
-
if (pathParts.length >= 2) {
|
|
27
|
-
const owner = pathParts[pathParts.length - 2]
|
|
28
|
-
const repo = pathParts[pathParts.length - 1].replace(/\.git$/, '')
|
|
29
|
-
return { owner, repo }
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
return null
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseGitConfigForIdentity(configPath: string): GitRepositoryIdentity | null {
|
|
41
|
-
let cfg = ''
|
|
42
|
-
try {
|
|
43
|
-
cfg = fsSync.readFileSync(configPath, 'utf8')
|
|
44
|
-
} catch {
|
|
45
|
-
return null
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
for (const line of cfg.split(/\r?\n/)) {
|
|
49
|
-
const trimmed = line.trim()
|
|
50
|
-
if (!trimmed.startsWith('url')) {
|
|
51
|
-
continue
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const match = trimmed.match(/^url\s*=\s*(.+?)\s*$/)
|
|
55
|
-
if (!match) {
|
|
56
|
-
continue
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const parsed = parseGitRemoteUrl(match[1].trim())
|
|
60
|
-
if (parsed) {
|
|
61
|
-
return parsed
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return null
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function parseGitDirFromDotGit(dotGitPath: string): string | null {
|
|
69
|
-
let contents = ''
|
|
70
|
-
try {
|
|
71
|
-
contents = fsSync.readFileSync(dotGitPath, 'utf8')
|
|
72
|
-
} catch {
|
|
73
|
-
return null
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const trimmed = contents.trim()
|
|
77
|
-
const gitdirMatch = trimmed.match(/^gitdir:\s*(.+)$/i)
|
|
78
|
-
if (!gitdirMatch) {
|
|
79
|
-
return null
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const gitdir = gitdirMatch[1].trim()
|
|
83
|
-
if (!gitdir) {
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (path.isAbsolute(gitdir)) {
|
|
88
|
-
return gitdir
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let cursor = path.dirname(dotGitPath)
|
|
92
|
-
while (true) {
|
|
93
|
-
const candidate = path.resolve(cursor, gitdir)
|
|
94
|
-
try {
|
|
95
|
-
const stat = fsSync.statSync(candidate)
|
|
96
|
-
if (stat.isDirectory()) {
|
|
97
|
-
return candidate
|
|
98
|
-
}
|
|
99
|
-
} catch {}
|
|
100
|
-
|
|
101
|
-
const next = path.dirname(cursor)
|
|
102
|
-
if (next === cursor) {
|
|
103
|
-
break
|
|
104
|
-
}
|
|
105
|
-
cursor = next
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return path.resolve(path.dirname(dotGitPath), gitdir)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function resolveGitConfigPath(dotGitPath: string): string | null {
|
|
112
|
-
let stat: fsSync.Stats
|
|
113
|
-
try {
|
|
114
|
-
stat = fsSync.statSync(dotGitPath)
|
|
115
|
-
} catch {
|
|
116
|
-
return null
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (stat.isDirectory()) {
|
|
120
|
-
return path.join(dotGitPath, 'config')
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (stat.isFile()) {
|
|
124
|
-
const gitDir = parseGitDirFromDotGit(dotGitPath)
|
|
125
|
-
if (!gitDir) return null
|
|
126
|
-
return path.join(gitDir, 'config')
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return null
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function resolveProjectRepoIdentity(projectRoot: string): GitRepositoryIdentity | null {
|
|
133
|
-
const candidates = [
|
|
134
|
-
path.join(projectRoot, 'code', '.git'),
|
|
135
|
-
path.join(projectRoot, '.git'),
|
|
136
|
-
]
|
|
137
|
-
|
|
138
|
-
for (const candidate of candidates) {
|
|
139
|
-
const configPath = resolveGitConfigPath(candidate)
|
|
140
|
-
if (!configPath) {
|
|
141
|
-
continue
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const parsed = parseGitConfigForIdentity(configPath)
|
|
145
|
-
if (parsed) {
|
|
146
|
-
return parsed
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return null
|
|
151
|
-
}
|
|
1
|
+
import fsSync from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface GitRepositoryIdentity {
|
|
5
|
+
owner: string
|
|
6
|
+
repo: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseGitRemoteUrl(remoteUrl: string): GitRepositoryIdentity | null {
|
|
10
|
+
try {
|
|
11
|
+
if (remoteUrl.startsWith('http://') || remoteUrl.startsWith('https://') || remoteUrl.startsWith('ssh://')) {
|
|
12
|
+
const parsed = new URL(remoteUrl)
|
|
13
|
+
const segments = parsed.pathname.split('/').filter(Boolean)
|
|
14
|
+
if (segments.length >= 2) {
|
|
15
|
+
const owner = segments[segments.length - 2]
|
|
16
|
+
const rawRepo = segments[segments.length - 1]
|
|
17
|
+
const repo = rawRepo.replace(/\.git$/, '')
|
|
18
|
+
return { owner, repo }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (remoteUrl.startsWith('git@')) {
|
|
23
|
+
const parts = remoteUrl.split(':', 2)
|
|
24
|
+
if (parts.length === 2) {
|
|
25
|
+
const pathParts = parts[1].split('/').filter(Boolean)
|
|
26
|
+
if (pathParts.length >= 2) {
|
|
27
|
+
const owner = pathParts[pathParts.length - 2]
|
|
28
|
+
const repo = pathParts[pathParts.length - 1].replace(/\.git$/, '')
|
|
29
|
+
return { owner, repo }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseGitConfigForIdentity(configPath: string): GitRepositoryIdentity | null {
|
|
41
|
+
let cfg = ''
|
|
42
|
+
try {
|
|
43
|
+
cfg = fsSync.readFileSync(configPath, 'utf8')
|
|
44
|
+
} catch {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const line of cfg.split(/\r?\n/)) {
|
|
49
|
+
const trimmed = line.trim()
|
|
50
|
+
if (!trimmed.startsWith('url')) {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const match = trimmed.match(/^url\s*=\s*(.+?)\s*$/)
|
|
55
|
+
if (!match) {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const parsed = parseGitRemoteUrl(match[1].trim())
|
|
60
|
+
if (parsed) {
|
|
61
|
+
return parsed
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseGitDirFromDotGit(dotGitPath: string): string | null {
|
|
69
|
+
let contents = ''
|
|
70
|
+
try {
|
|
71
|
+
contents = fsSync.readFileSync(dotGitPath, 'utf8')
|
|
72
|
+
} catch {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const trimmed = contents.trim()
|
|
77
|
+
const gitdirMatch = trimmed.match(/^gitdir:\s*(.+)$/i)
|
|
78
|
+
if (!gitdirMatch) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const gitdir = gitdirMatch[1].trim()
|
|
83
|
+
if (!gitdir) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (path.isAbsolute(gitdir)) {
|
|
88
|
+
return gitdir
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let cursor = path.dirname(dotGitPath)
|
|
92
|
+
while (true) {
|
|
93
|
+
const candidate = path.resolve(cursor, gitdir)
|
|
94
|
+
try {
|
|
95
|
+
const stat = fsSync.statSync(candidate)
|
|
96
|
+
if (stat.isDirectory()) {
|
|
97
|
+
return candidate
|
|
98
|
+
}
|
|
99
|
+
} catch {}
|
|
100
|
+
|
|
101
|
+
const next = path.dirname(cursor)
|
|
102
|
+
if (next === cursor) {
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
cursor = next
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return path.resolve(path.dirname(dotGitPath), gitdir)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveGitConfigPath(dotGitPath: string): string | null {
|
|
112
|
+
let stat: fsSync.Stats
|
|
113
|
+
try {
|
|
114
|
+
stat = fsSync.statSync(dotGitPath)
|
|
115
|
+
} catch {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (stat.isDirectory()) {
|
|
120
|
+
return path.join(dotGitPath, 'config')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (stat.isFile()) {
|
|
124
|
+
const gitDir = parseGitDirFromDotGit(dotGitPath)
|
|
125
|
+
if (!gitDir) return null
|
|
126
|
+
return path.join(gitDir, 'config')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function resolveProjectRepoIdentity(projectRoot: string): GitRepositoryIdentity | null {
|
|
133
|
+
const candidates = [
|
|
134
|
+
path.join(projectRoot, 'code', '.git'),
|
|
135
|
+
path.join(projectRoot, '.git'),
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
for (const candidate of candidates) {
|
|
139
|
+
const configPath = resolveGitConfigPath(candidate)
|
|
140
|
+
if (!configPath) {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const parsed = parseGitConfigForIdentity(configPath)
|
|
145
|
+
if (parsed) {
|
|
146
|
+
return parsed
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null
|
|
151
|
+
}
|
package/src/spec-mock.ts
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
export type GitApiPath = string[]
|
|
2
|
-
export type GitApiQuery = string[]
|
|
3
|
-
export type GitApiHeaders = string[]
|
|
4
|
-
|
|
5
|
-
export interface GitApiMockResponse {
|
|
6
|
-
endpoint: string
|
|
7
|
-
method: string
|
|
8
|
-
path: GitApiPath
|
|
9
|
-
query: GitApiQuery
|
|
10
|
-
headers: GitApiHeaders
|
|
11
|
-
requestParts: string[]
|
|
12
|
-
apiVersion: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const GIT_API_VERSION = '2022-11-28'
|
|
16
|
-
|
|
17
|
-
export interface MockInvocation {
|
|
18
|
-
path: GitApiPath
|
|
19
|
-
method?: string
|
|
20
|
-
query?: GitApiQuery
|
|
21
|
-
headers?: GitApiHeaders
|
|
22
|
-
apiBase?: string
|
|
23
|
-
apiVersion?: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function buildGitApiMockResponse(invocation: MockInvocation): GitApiMockResponse {
|
|
27
|
-
const method = invocation.method ?? 'GET'
|
|
28
|
-
const path = invocation.path
|
|
29
|
-
const query = invocation.query ?? []
|
|
30
|
-
const headers = invocation.headers ?? []
|
|
31
|
-
const apiBase = invocation.apiBase ?? 'https://api.github.com'
|
|
32
|
-
const apiVersion = invocation.apiVersion ?? GIT_API_VERSION
|
|
33
|
-
const requestParts = [method, ...path, ...query, ...headers]
|
|
34
|
-
const endpoint = path.length > 0 ? `${apiBase}/${path.join('/')}` : apiBase
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
endpoint,
|
|
38
|
-
method,
|
|
39
|
-
path: [...path],
|
|
40
|
-
query: [...query],
|
|
41
|
-
headers: [...headers],
|
|
42
|
-
requestParts,
|
|
43
|
-
apiVersion,
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
export type GitApiPath = string[]
|
|
2
|
+
export type GitApiQuery = string[]
|
|
3
|
+
export type GitApiHeaders = string[]
|
|
4
|
+
|
|
5
|
+
export interface GitApiMockResponse {
|
|
6
|
+
endpoint: string
|
|
7
|
+
method: string
|
|
8
|
+
path: GitApiPath
|
|
9
|
+
query: GitApiQuery
|
|
10
|
+
headers: GitApiHeaders
|
|
11
|
+
requestParts: string[]
|
|
12
|
+
apiVersion: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GIT_API_VERSION = '2022-11-28'
|
|
16
|
+
|
|
17
|
+
export interface MockInvocation {
|
|
18
|
+
path: GitApiPath
|
|
19
|
+
method?: string
|
|
20
|
+
query?: GitApiQuery
|
|
21
|
+
headers?: GitApiHeaders
|
|
22
|
+
apiBase?: string
|
|
23
|
+
apiVersion?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildGitApiMockResponse(invocation: MockInvocation): GitApiMockResponse {
|
|
27
|
+
const method = invocation.method ?? 'GET'
|
|
28
|
+
const path = invocation.path
|
|
29
|
+
const query = invocation.query ?? []
|
|
30
|
+
const headers = invocation.headers ?? []
|
|
31
|
+
const apiBase = invocation.apiBase ?? 'https://api.github.com'
|
|
32
|
+
const apiVersion = invocation.apiVersion ?? GIT_API_VERSION
|
|
33
|
+
const requestParts = [method, ...path, ...query, ...headers]
|
|
34
|
+
const endpoint = path.length > 0 ? `${apiBase}/${path.join('/')}` : apiBase
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
endpoint,
|
|
38
|
+
method,
|
|
39
|
+
path: [...path],
|
|
40
|
+
query: [...query],
|
|
41
|
+
headers: [...headers],
|
|
42
|
+
requestParts,
|
|
43
|
+
apiVersion,
|
|
44
|
+
}
|
|
45
|
+
}
|