@foundation0/git 1.2.4 → 1.3.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.
@@ -79,15 +79,44 @@ const fetchWithTimeout = async (url: string, init: RequestInit): Promise<Respons
79
79
  const readStream = async (stream: NodeJS.ReadableStream | null): Promise<Buffer> => {
80
80
  if (!stream) return Buffer.from([])
81
81
 
82
- const chunks: Buffer[] = []
83
- for await (const chunk of stream) {
84
- if (typeof chunk === 'string') {
85
- chunks.push(Buffer.from(chunk))
86
- } else {
87
- chunks.push(Buffer.from(chunk as ArrayBufferView))
82
+ return await new Promise<Buffer>((resolve) => {
83
+ const chunks: Buffer[] = []
84
+ let settled = false
85
+
86
+ const cleanup = () => {
87
+ stream.removeListener('data', onData)
88
+ stream.removeListener('end', onDone)
89
+ stream.removeListener('close', onDone)
90
+ stream.removeListener('error', onDone)
88
91
  }
89
- }
90
- return Buffer.concat(chunks)
92
+
93
+ const settle = () => {
94
+ if (settled) return
95
+ settled = true
96
+ cleanup()
97
+ resolve(Buffer.concat(chunks))
98
+ }
99
+
100
+ const onDone = () => settle()
101
+
102
+ const onData = (chunk: unknown) => {
103
+ try {
104
+ if (typeof chunk === 'string') {
105
+ chunks.push(Buffer.from(chunk))
106
+ return
107
+ }
108
+
109
+ chunks.push(Buffer.from(chunk as ArrayBufferView))
110
+ } catch {
111
+ // best effort
112
+ }
113
+ }
114
+
115
+ stream.on('data', onData)
116
+ stream.on('end', onDone)
117
+ stream.on('close', onDone)
118
+ stream.on('error', onDone)
119
+ })
91
120
  }
92
121
 
93
122
  const callCurl = async (
@@ -111,6 +140,7 @@ const callCurl = async (
111
140
  const timeoutSeconds = requestTimeoutMs > 0 ? Math.max(1, Math.ceil(requestTimeoutMs / 1000)) : null
112
141
  if (timeoutSeconds !== null) {
113
142
  args.push('--max-time', String(timeoutSeconds))
143
+ args.push('--connect-timeout', String(Math.max(1, Math.min(30, timeoutSeconds))))
114
144
  }
115
145
 
116
146
  for (const [name, value] of Object.entries(init.headers)) {
@@ -128,20 +158,54 @@ const callCurl = async (
128
158
  windowsHide: true,
129
159
  })
130
160
 
161
+ const hardTimeoutMs = requestTimeoutMs > 0 ? requestTimeoutMs + 2_000 : null
162
+ let hardTimedOut = false
163
+ const hardTimeoutId = hardTimeoutMs
164
+ ? setTimeout(() => {
165
+ hardTimedOut = true
166
+ try {
167
+ child.kill()
168
+ } catch {
169
+ // best effort
170
+ }
171
+ try {
172
+ child.stdout?.destroy()
173
+ } catch {
174
+ // best effort
175
+ }
176
+ try {
177
+ child.stderr?.destroy()
178
+ } catch {
179
+ // best effort
180
+ }
181
+ }, hardTimeoutMs)
182
+ : null
183
+
131
184
  if (init.body !== undefined) {
132
185
  child.stdin.write(init.body)
133
186
  }
134
187
  child.stdin.end()
135
188
 
136
- const [stdoutBytes, stderrBytes] = await Promise.all([
137
- readStream(child.stdout),
138
- readStream(child.stderr),
139
- ])
189
+ const stdoutPromise = readStream(child.stdout)
190
+ const stderrPromise = readStream(child.stderr)
140
191
 
141
- const exitCode: number = await new Promise((resolve) => {
142
- child.on('close', (code) => resolve(code ?? 0))
143
- child.on('error', () => resolve(1))
144
- })
192
+ let exitCode: number
193
+ try {
194
+ exitCode = await new Promise((resolve) => {
195
+ child.on('close', (code) => resolve(code ?? 0))
196
+ child.on('error', () => resolve(1))
197
+ })
198
+ } finally {
199
+ if (hardTimeoutId) {
200
+ clearTimeout(hardTimeoutId)
201
+ }
202
+ }
203
+
204
+ const [stdoutBytes, stderrBytes] = await Promise.all([stdoutPromise, stderrPromise])
205
+
206
+ if (hardTimedOut && requestTimeoutMs > 0) {
207
+ throw new Error(`Request timed out after ${requestTimeoutMs}ms: ${init.method} ${requestUrl}`)
208
+ }
145
209
 
146
210
  const stdout = stdoutBytes.toString('utf8')
147
211
  const stderr = stderrBytes.toString('utf8').trim()
@@ -386,6 +386,11 @@ export class GiteaPlatformAdapter {
386
386
  return values
387
387
  }
388
388
 
389
+ const isRepoScoped =
390
+ segments.length >= 2 &&
391
+ segments[0] === '{owner}' &&
392
+ segments[1] === '{repo}'
393
+
389
394
  if (values[0].includes('/') && placeholderCount >= 2) {
390
395
  const split = values[0].split('/')
391
396
  if (split.length >= 2 && split[0] && split[1]) {
@@ -394,12 +399,19 @@ export class GiteaPlatformAdapter {
394
399
  }
395
400
  }
396
401
 
397
- if (placeholderCount >= 2 && values.length >= 2) {
398
- return values
402
+ // For repo-scoped commands, allow passing the "rest" of the arguments (number/id/path) without
403
+ // explicitly providing owner/repo. The git-service layer can hydrate {owner}/{repo} from defaults.
404
+ //
405
+ // Example: swaggerPath "/repos/{owner}/{repo}/pulls/{number}" with args ["123"] should map to:
406
+ // owner="{owner}" repo="{repo}" number="123"
407
+ //
408
+ // We achieve this by left-padding args with empty strings so mapSwaggerPath leaves placeholders intact.
409
+ if (isRepoScoped && placeholderCount > 2 && values.length === 1 && !values[0].includes('/')) {
410
+ values.unshift('', '')
399
411
  }
400
412
 
401
- while (values.length < placeholderCount) {
402
- values.push(`{v${values.length}}`)
413
+ if (placeholderCount >= 2 && values.length >= 2) {
414
+ return values
403
415
  }
404
416
 
405
417
  return values