@mcp-consultant-tools/github-enterprise 1.0.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/build/GitHubEnterpriseService.d.ts +179 -0
- package/build/GitHubEnterpriseService.d.ts.map +1 -0
- package/build/GitHubEnterpriseService.js +1101 -0
- package/build/GitHubEnterpriseService.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +17 -0
- package/build/index.js.map +1 -0
- package/build/utils/ghe-formatters.d.ts +53 -0
- package/build/utils/ghe-formatters.d.ts.map +1 -0
- package/build/utils/ghe-formatters.js +331 -0
- package/build/utils/ghe-formatters.js.map +1 -0
- package/package.json +26 -0
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
import { Octokit } from '@octokit/rest';
|
|
2
|
+
import { createAppAuth } from '@octokit/auth-app';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { auditLogger } from '@mcp-consultant-tools/core';
|
|
5
|
+
/**
|
|
6
|
+
* GitHub Enterprise Service
|
|
7
|
+
* Manages authentication, API requests, caching, and branch selection for GitHub Enterprise Cloud
|
|
8
|
+
*/
|
|
9
|
+
export class GitHubEnterpriseService {
|
|
10
|
+
config;
|
|
11
|
+
baseApiUrl;
|
|
12
|
+
octokit = null;
|
|
13
|
+
// Token caching (for GitHub App)
|
|
14
|
+
accessToken = null;
|
|
15
|
+
tokenExpirationTime = 0;
|
|
16
|
+
// Response caching
|
|
17
|
+
cache = new Map();
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.baseApiUrl = `${config.baseUrl}/api/v3`;
|
|
21
|
+
// Initialize Octokit based on auth method
|
|
22
|
+
this.initializeOctokit();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Initialize Octokit client based on authentication method
|
|
26
|
+
*/
|
|
27
|
+
initializeOctokit() {
|
|
28
|
+
try {
|
|
29
|
+
if (this.config.authMethod === 'pat') {
|
|
30
|
+
// PAT authentication (primary method)
|
|
31
|
+
this.octokit = new Octokit({
|
|
32
|
+
auth: this.config.pat,
|
|
33
|
+
baseUrl: this.baseApiUrl,
|
|
34
|
+
userAgent: 'mcp-consultant-tools',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else if (this.config.authMethod === 'github-app') {
|
|
38
|
+
// GitHub App authentication (optional/advanced)
|
|
39
|
+
if (!this.config.appId || !this.config.appPrivateKey || !this.config.appInstallationId) {
|
|
40
|
+
throw new Error('GitHub App authentication requires appId, appPrivateKey, and appInstallationId');
|
|
41
|
+
}
|
|
42
|
+
this.octokit = new Octokit({
|
|
43
|
+
authStrategy: createAppAuth,
|
|
44
|
+
auth: {
|
|
45
|
+
appId: this.config.appId,
|
|
46
|
+
privateKey: this.config.appPrivateKey,
|
|
47
|
+
installationId: this.config.appInstallationId,
|
|
48
|
+
},
|
|
49
|
+
baseUrl: this.baseApiUrl,
|
|
50
|
+
userAgent: 'mcp-consultant-tools',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new Error(`Unsupported authentication method: ${this.config.authMethod}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error('Failed to initialize Octokit:', error.message);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get access token with caching (for GitHub App auth)
|
|
64
|
+
* Implements 5-minute buffer pattern before expiry
|
|
65
|
+
*/
|
|
66
|
+
async getAccessToken() {
|
|
67
|
+
if (this.config.authMethod === 'pat') {
|
|
68
|
+
return this.config.pat;
|
|
69
|
+
}
|
|
70
|
+
const currentTime = Date.now();
|
|
71
|
+
// Return cached token if still valid (with 5 minute buffer)
|
|
72
|
+
if (this.accessToken && this.tokenExpirationTime > currentTime) {
|
|
73
|
+
return this.accessToken;
|
|
74
|
+
}
|
|
75
|
+
// Acquire new token for GitHub App
|
|
76
|
+
try {
|
|
77
|
+
const auth = await this.octokit.auth({ type: 'installation' });
|
|
78
|
+
if (!auth.token) {
|
|
79
|
+
throw new Error('GitHub App auth did not return a token');
|
|
80
|
+
}
|
|
81
|
+
const token = auth.token;
|
|
82
|
+
this.accessToken = token;
|
|
83
|
+
// GitHub App installation tokens expire after 1 hour
|
|
84
|
+
// Set expiration time (subtract 5 minutes to refresh early)
|
|
85
|
+
this.tokenExpirationTime = currentTime + (55 * 60 * 1000); // 55 minutes
|
|
86
|
+
return token;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error('Failed to acquire GitHub App installation token:', error.message);
|
|
90
|
+
throw new Error(`Failed to acquire GitHub App token: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get cache key for a request
|
|
95
|
+
*/
|
|
96
|
+
getCacheKey(method, repo, resource, params) {
|
|
97
|
+
const paramStr = params ? JSON.stringify(params) : '';
|
|
98
|
+
return `${method}:${repo}:${resource}:${paramStr}`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get cached response
|
|
102
|
+
*/
|
|
103
|
+
getCached(key) {
|
|
104
|
+
const cached = this.cache.get(key);
|
|
105
|
+
if (cached && Date.now() < cached.expires) {
|
|
106
|
+
return cached.data;
|
|
107
|
+
}
|
|
108
|
+
this.cache.delete(key); // Expired - remove it
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Set cache entry
|
|
113
|
+
*/
|
|
114
|
+
setCache(key, data, ttlSeconds) {
|
|
115
|
+
if (!this.config.enableCache)
|
|
116
|
+
return;
|
|
117
|
+
const ttl = ttlSeconds || this.config.cacheTtl;
|
|
118
|
+
this.cache.set(key, {
|
|
119
|
+
data,
|
|
120
|
+
expires: Date.now() + (ttl * 1000)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Clear cache entries
|
|
125
|
+
* @param pattern Optional pattern to match cache keys
|
|
126
|
+
* @param repoId Optional repo ID to clear cache for specific repo
|
|
127
|
+
* @returns Number of cache entries cleared
|
|
128
|
+
*/
|
|
129
|
+
clearCache(pattern, repoId) {
|
|
130
|
+
if (repoId) {
|
|
131
|
+
const repo = this.getRepoById(repoId);
|
|
132
|
+
const repoPattern = `${repo.owner}/${repo.repo}`;
|
|
133
|
+
pattern = pattern ? `${repoPattern}:${pattern}` : repoPattern;
|
|
134
|
+
}
|
|
135
|
+
if (pattern) {
|
|
136
|
+
let cleared = 0;
|
|
137
|
+
for (const key of this.cache.keys()) {
|
|
138
|
+
if (key.includes(pattern)) {
|
|
139
|
+
this.cache.delete(key);
|
|
140
|
+
cleared++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
console.error(`Cleared ${cleared} cache entries matching pattern '${pattern}'`);
|
|
144
|
+
return cleared;
|
|
145
|
+
}
|
|
146
|
+
const size = this.cache.size;
|
|
147
|
+
this.cache.clear();
|
|
148
|
+
console.error(`Cleared all ${size} cache entries`);
|
|
149
|
+
return size;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Make API request with error handling and caching
|
|
153
|
+
*/
|
|
154
|
+
async makeRequest(endpoint, options = {}) {
|
|
155
|
+
const { method = 'GET', data, useCache = true, cacheTtl, repoId } = options;
|
|
156
|
+
// Check cache for GET requests
|
|
157
|
+
if (method === 'GET' && useCache && this.config.enableCache) {
|
|
158
|
+
const cacheKey = this.getCacheKey(method, repoId || '', endpoint, data);
|
|
159
|
+
const cached = this.getCached(cacheKey);
|
|
160
|
+
if (cached) {
|
|
161
|
+
return cached;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const token = await this.getAccessToken();
|
|
166
|
+
const url = endpoint.startsWith('http') ? endpoint : `${this.baseApiUrl}/${endpoint}`;
|
|
167
|
+
const response = await axios({
|
|
168
|
+
method,
|
|
169
|
+
url,
|
|
170
|
+
headers: {
|
|
171
|
+
'Authorization': `token ${token}`,
|
|
172
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
173
|
+
'X-GitHub-Api-Version': this.config.apiVersion,
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
},
|
|
176
|
+
data,
|
|
177
|
+
});
|
|
178
|
+
// Cache successful GET responses
|
|
179
|
+
if (method === 'GET' && useCache && this.config.enableCache) {
|
|
180
|
+
const cacheKey = this.getCacheKey(method, repoId || '', endpoint, data);
|
|
181
|
+
this.setCache(cacheKey, response.data, cacheTtl);
|
|
182
|
+
}
|
|
183
|
+
return response.data;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
// Comprehensive error handling
|
|
187
|
+
let errorMessage = 'Unknown error';
|
|
188
|
+
let errorDetails = {};
|
|
189
|
+
if (error.response) {
|
|
190
|
+
const status = error.response.status;
|
|
191
|
+
const data = error.response.data;
|
|
192
|
+
switch (status) {
|
|
193
|
+
case 401:
|
|
194
|
+
errorMessage = 'Authentication failed. Check your PAT or GitHub App credentials.';
|
|
195
|
+
break;
|
|
196
|
+
case 403:
|
|
197
|
+
if (error.response.headers['x-ratelimit-remaining'] === '0') {
|
|
198
|
+
const resetTime = error.response.headers['x-ratelimit-reset'];
|
|
199
|
+
const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleString() : 'unknown';
|
|
200
|
+
errorMessage = `Rate limit exceeded. Resets at ${resetDate}.`;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
errorMessage = 'Access denied. Check repository permissions.';
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
case 404:
|
|
207
|
+
errorMessage = `Resource not found: ${endpoint}`;
|
|
208
|
+
break;
|
|
209
|
+
case 422:
|
|
210
|
+
errorMessage = `Validation failed: ${data?.message || 'Invalid request parameters'}`;
|
|
211
|
+
break;
|
|
212
|
+
default:
|
|
213
|
+
errorMessage = `HTTP ${status}: ${data?.message || error.message}`;
|
|
214
|
+
}
|
|
215
|
+
errorDetails = { status, message: data?.message };
|
|
216
|
+
}
|
|
217
|
+
else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
218
|
+
errorMessage = `Network error: Unable to reach GitHub Enterprise at ${this.config.baseUrl}. Check your connection and GHE_URL.`;
|
|
219
|
+
}
|
|
220
|
+
else if (error.code === 'ETIMEDOUT') {
|
|
221
|
+
errorMessage = 'Request timeout. GitHub Enterprise API is slow to respond.';
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
errorMessage = error.message;
|
|
225
|
+
}
|
|
226
|
+
console.error('GitHub Enterprise API request failed:', { endpoint, method, status: error.response?.status, error: errorMessage });
|
|
227
|
+
throw new Error(errorMessage);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get all configured repositories
|
|
232
|
+
*/
|
|
233
|
+
getAllRepos() {
|
|
234
|
+
return this.config.repos;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get active repositories only
|
|
238
|
+
*/
|
|
239
|
+
getActiveRepos() {
|
|
240
|
+
return this.config.repos.filter(r => r.active);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get repository by ID with validation
|
|
244
|
+
*/
|
|
245
|
+
getRepoById(repoId) {
|
|
246
|
+
const repo = this.config.repos.find(r => r.id === repoId);
|
|
247
|
+
if (!repo) {
|
|
248
|
+
const availableIds = this.config.repos.map(r => r.id).join(', ');
|
|
249
|
+
throw new Error(`Repository '${repoId}' not found. Available repositories: ${availableIds || 'none'}`);
|
|
250
|
+
}
|
|
251
|
+
if (!repo.active) {
|
|
252
|
+
throw new Error(`Repository '${repoId}' is inactive. Set 'active: true' in configuration to enable it.`);
|
|
253
|
+
}
|
|
254
|
+
return repo;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* List all branches for a repository
|
|
258
|
+
*/
|
|
259
|
+
async listBranches(repoId, protectedOnly) {
|
|
260
|
+
const timer = auditLogger.startTimer();
|
|
261
|
+
const repo = this.getRepoById(repoId);
|
|
262
|
+
try {
|
|
263
|
+
const branches = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/branches`, { repoId });
|
|
264
|
+
const filteredBranches = protectedOnly !== undefined
|
|
265
|
+
? branches.filter(b => b.protected === protectedOnly)
|
|
266
|
+
: branches;
|
|
267
|
+
auditLogger.log({
|
|
268
|
+
operation: 'list-branches',
|
|
269
|
+
operationType: 'READ',
|
|
270
|
+
componentType: 'Branch',
|
|
271
|
+
success: true,
|
|
272
|
+
parameters: { repoId, protectedOnly },
|
|
273
|
+
executionTimeMs: timer(),
|
|
274
|
+
});
|
|
275
|
+
return filteredBranches;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
auditLogger.log({
|
|
279
|
+
operation: 'list-branches',
|
|
280
|
+
operationType: 'READ',
|
|
281
|
+
componentType: 'Branch',
|
|
282
|
+
success: false,
|
|
283
|
+
error: error.message,
|
|
284
|
+
parameters: { repoId },
|
|
285
|
+
executionTimeMs: timer(),
|
|
286
|
+
});
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Auto-detect default branch for a repository
|
|
292
|
+
* Handles typos gracefully and provides alternatives
|
|
293
|
+
*/
|
|
294
|
+
async getDefaultBranch(repoId, userSpecified) {
|
|
295
|
+
const repo = this.getRepoById(repoId);
|
|
296
|
+
// 1. User explicitly specified branch (highest priority)
|
|
297
|
+
if (userSpecified) {
|
|
298
|
+
const branches = await this.listBranches(repoId);
|
|
299
|
+
const exists = branches.find(b => b.name === userSpecified);
|
|
300
|
+
if (exists) {
|
|
301
|
+
return {
|
|
302
|
+
branch: userSpecified,
|
|
303
|
+
reason: 'user-specified',
|
|
304
|
+
confidence: 'high'
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Branch doesn't exist - show available branches
|
|
308
|
+
const availableBranches = branches.map(b => ` - ${b.name}`).join('\n');
|
|
309
|
+
throw new Error(`Branch "${userSpecified}" not found in ${repo.owner}/${repo.repo}.\n\n` +
|
|
310
|
+
`Available branches:\n${availableBranches}`);
|
|
311
|
+
}
|
|
312
|
+
// 2. Check if default branch configured for this repo
|
|
313
|
+
if (repo.defaultBranch) {
|
|
314
|
+
return {
|
|
315
|
+
branch: repo.defaultBranch,
|
|
316
|
+
reason: 'configured default',
|
|
317
|
+
confidence: 'high'
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// 3. Get all branches
|
|
321
|
+
const branches = await this.listBranches(repoId);
|
|
322
|
+
// 4. Filter and sort release branches (handle typos gracefully)
|
|
323
|
+
const releaseBranches = branches
|
|
324
|
+
.filter(b => b.name.toLowerCase().startsWith('release/')) // Case-insensitive
|
|
325
|
+
.map(b => {
|
|
326
|
+
// Parse version number after "release/"
|
|
327
|
+
const versionStr = b.name.substring(b.name.indexOf('/') + 1);
|
|
328
|
+
const version = parseFloat(versionStr);
|
|
329
|
+
return {
|
|
330
|
+
name: b.name,
|
|
331
|
+
version: isNaN(version) ? 0 : version,
|
|
332
|
+
raw: versionStr
|
|
333
|
+
};
|
|
334
|
+
})
|
|
335
|
+
.filter(b => b.version > 0) // Only keep valid version numbers
|
|
336
|
+
.sort((a, b) => b.version - a.version); // Highest first
|
|
337
|
+
// 5. Auto-select highest version, but ALWAYS show alternatives
|
|
338
|
+
if (releaseBranches.length > 0) {
|
|
339
|
+
const selected = releaseBranches[0].name;
|
|
340
|
+
const allAlternatives = releaseBranches.slice(1).map(b => b.name);
|
|
341
|
+
console.error(`✓ Auto-selected branch: ${selected} (highest release version ${releaseBranches[0].version})`);
|
|
342
|
+
if (allAlternatives.length > 0) {
|
|
343
|
+
console.error(` Alternatives: ${allAlternatives.slice(0, 3).join(', ')}${allAlternatives.length > 3 ? '...' : ''}`);
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
branch: selected,
|
|
347
|
+
reason: `auto-detected: highest release version (${releaseBranches[0].version})`,
|
|
348
|
+
confidence: 'medium',
|
|
349
|
+
alternatives: allAlternatives,
|
|
350
|
+
message: `Auto-selected "${selected}". If this is incorrect, specify a different branch explicitly.`
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// 6. No release branches found - fallback to main/master
|
|
354
|
+
console.error(`⚠️ No release branches found in format "release/X.Y" for ${repo.owner}/${repo.repo}`);
|
|
355
|
+
const availableBranchNames = branches.map(b => b.name);
|
|
356
|
+
console.error(` Available branches: ${availableBranchNames.slice(0, 5).join(', ')}${availableBranchNames.length > 5 ? '...' : ''}`);
|
|
357
|
+
const mainBranch = branches.find(b => b.name === 'main' || b.name === 'master');
|
|
358
|
+
if (mainBranch) {
|
|
359
|
+
console.error(`⚠️ Falling back to: ${mainBranch.name} (main branch - likely production)`);
|
|
360
|
+
return {
|
|
361
|
+
branch: mainBranch.name,
|
|
362
|
+
reason: 'fallback to main branch (no release branches found)',
|
|
363
|
+
confidence: 'low',
|
|
364
|
+
alternatives: availableBranchNames.filter(n => n !== mainBranch.name),
|
|
365
|
+
message: `No release branches found. Using "${mainBranch.name}" as fallback. User should verify this is correct.`
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// 7. Cannot determine - list all branches and throw error
|
|
369
|
+
const branchList = availableBranchNames.map(n => ` - ${n}`).join('\n');
|
|
370
|
+
throw new Error(`Could not determine default branch for ${repo.owner}/${repo.repo}.\n\n` +
|
|
371
|
+
`Available branches:\n${branchList}\n\n` +
|
|
372
|
+
`Please specify a branch explicitly or configure a defaultBranch in GHE_REPOS.`);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get file content from a repository
|
|
376
|
+
*/
|
|
377
|
+
async getFile(repoId, path, branch) {
|
|
378
|
+
const timer = auditLogger.startTimer();
|
|
379
|
+
const repo = this.getRepoById(repoId);
|
|
380
|
+
try {
|
|
381
|
+
// Auto-detect branch if not specified
|
|
382
|
+
const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch;
|
|
383
|
+
const file = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${path}?ref=${selectedBranch}`, { repoId });
|
|
384
|
+
// Check file size
|
|
385
|
+
if (file.size > this.config.maxFileSize) {
|
|
386
|
+
throw new Error(`File size (${file.size} bytes) exceeds maximum allowed size (${this.config.maxFileSize} bytes). ` +
|
|
387
|
+
`Increase GHE_MAX_FILE_SIZE if needed.`);
|
|
388
|
+
}
|
|
389
|
+
// Decode base64 content
|
|
390
|
+
if (file.encoding === 'base64') {
|
|
391
|
+
file.decodedContent = Buffer.from(file.content, 'base64').toString('utf-8');
|
|
392
|
+
}
|
|
393
|
+
auditLogger.log({
|
|
394
|
+
operation: 'get-file',
|
|
395
|
+
operationType: 'READ',
|
|
396
|
+
componentType: 'File',
|
|
397
|
+
componentName: path,
|
|
398
|
+
success: true,
|
|
399
|
+
parameters: { repoId, path, branch: selectedBranch },
|
|
400
|
+
executionTimeMs: timer(),
|
|
401
|
+
});
|
|
402
|
+
return { ...file, branch: selectedBranch };
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
auditLogger.log({
|
|
406
|
+
operation: 'get-file',
|
|
407
|
+
operationType: 'READ',
|
|
408
|
+
componentType: 'File',
|
|
409
|
+
componentName: path,
|
|
410
|
+
success: false,
|
|
411
|
+
error: error.message,
|
|
412
|
+
parameters: { repoId, path, branch },
|
|
413
|
+
executionTimeMs: timer(),
|
|
414
|
+
});
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Search code across repositories
|
|
420
|
+
*/
|
|
421
|
+
async searchCode(query, repoId, path, extension) {
|
|
422
|
+
const timer = auditLogger.startTimer();
|
|
423
|
+
try {
|
|
424
|
+
// Build search query
|
|
425
|
+
let searchQuery = query;
|
|
426
|
+
if (repoId) {
|
|
427
|
+
const repo = this.getRepoById(repoId);
|
|
428
|
+
searchQuery += ` repo:${repo.owner}/${repo.repo}`;
|
|
429
|
+
}
|
|
430
|
+
if (path) {
|
|
431
|
+
searchQuery += ` path:${path}`;
|
|
432
|
+
}
|
|
433
|
+
if (extension) {
|
|
434
|
+
searchQuery += ` extension:${extension}`;
|
|
435
|
+
}
|
|
436
|
+
const result = await this.makeRequest(`search/code?q=${encodeURIComponent(searchQuery)}&per_page=${this.config.maxSearchResults}`, { useCache: false } // Don't cache search results
|
|
437
|
+
);
|
|
438
|
+
auditLogger.log({
|
|
439
|
+
operation: 'search-code',
|
|
440
|
+
operationType: 'READ',
|
|
441
|
+
componentType: 'Code',
|
|
442
|
+
success: true,
|
|
443
|
+
parameters: { query, repoId, path, extension, totalResults: result.total_count },
|
|
444
|
+
executionTimeMs: timer(),
|
|
445
|
+
});
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
auditLogger.log({
|
|
450
|
+
operation: 'search-code',
|
|
451
|
+
operationType: 'READ',
|
|
452
|
+
componentType: 'Code',
|
|
453
|
+
success: false,
|
|
454
|
+
error: error.message,
|
|
455
|
+
parameters: { query, repoId, path, extension },
|
|
456
|
+
executionTimeMs: timer(),
|
|
457
|
+
});
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* List files in a directory
|
|
463
|
+
*/
|
|
464
|
+
async listFiles(repoId, path, branch) {
|
|
465
|
+
const timer = auditLogger.startTimer();
|
|
466
|
+
const repo = this.getRepoById(repoId);
|
|
467
|
+
try {
|
|
468
|
+
// Auto-detect branch if not specified
|
|
469
|
+
const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch;
|
|
470
|
+
const dirPath = path || '';
|
|
471
|
+
const contents = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${dirPath}?ref=${selectedBranch}`, { repoId });
|
|
472
|
+
auditLogger.log({
|
|
473
|
+
operation: 'list-files',
|
|
474
|
+
operationType: 'READ',
|
|
475
|
+
componentType: 'Directory',
|
|
476
|
+
componentName: path || '/',
|
|
477
|
+
success: true,
|
|
478
|
+
parameters: { repoId, path, branch: selectedBranch },
|
|
479
|
+
executionTimeMs: timer(),
|
|
480
|
+
});
|
|
481
|
+
return { contents, branch: selectedBranch };
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
auditLogger.log({
|
|
485
|
+
operation: 'list-files',
|
|
486
|
+
operationType: 'READ',
|
|
487
|
+
componentType: 'Directory',
|
|
488
|
+
componentName: path || '/',
|
|
489
|
+
success: false,
|
|
490
|
+
error: error.message,
|
|
491
|
+
parameters: { repoId, path, branch },
|
|
492
|
+
executionTimeMs: timer(),
|
|
493
|
+
});
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get commit history for a branch
|
|
499
|
+
*/
|
|
500
|
+
async getCommits(repoId, branch, since, until, author, path, limit = 50) {
|
|
501
|
+
const timer = auditLogger.startTimer();
|
|
502
|
+
const repo = this.getRepoById(repoId);
|
|
503
|
+
try {
|
|
504
|
+
// Auto-detect branch if not specified
|
|
505
|
+
const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch;
|
|
506
|
+
// Build query parameters
|
|
507
|
+
const params = {
|
|
508
|
+
sha: selectedBranch,
|
|
509
|
+
per_page: limit,
|
|
510
|
+
};
|
|
511
|
+
if (since)
|
|
512
|
+
params.since = since;
|
|
513
|
+
if (until)
|
|
514
|
+
params.until = until;
|
|
515
|
+
if (author)
|
|
516
|
+
params.author = author;
|
|
517
|
+
if (path)
|
|
518
|
+
params.path = path;
|
|
519
|
+
const queryString = new URLSearchParams(params).toString();
|
|
520
|
+
const commits = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/commits?${queryString}`, { repoId });
|
|
521
|
+
auditLogger.log({
|
|
522
|
+
operation: 'get-commits',
|
|
523
|
+
operationType: 'READ',
|
|
524
|
+
componentType: 'Commit',
|
|
525
|
+
success: true,
|
|
526
|
+
parameters: { repoId, branch: selectedBranch, since, until, author, path, limit, count: commits.length },
|
|
527
|
+
executionTimeMs: timer(),
|
|
528
|
+
});
|
|
529
|
+
return commits;
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
auditLogger.log({
|
|
533
|
+
operation: 'get-commits',
|
|
534
|
+
operationType: 'READ',
|
|
535
|
+
componentType: 'Commit',
|
|
536
|
+
success: false,
|
|
537
|
+
error: error.message,
|
|
538
|
+
parameters: { repoId, branch, since, until, author, path, limit },
|
|
539
|
+
executionTimeMs: timer(),
|
|
540
|
+
});
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Get commit details
|
|
546
|
+
*/
|
|
547
|
+
async getCommitDetails(repoId, sha) {
|
|
548
|
+
const timer = auditLogger.startTimer();
|
|
549
|
+
const repo = this.getRepoById(repoId);
|
|
550
|
+
try {
|
|
551
|
+
const commit = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/commits/${sha}`, { repoId });
|
|
552
|
+
auditLogger.log({
|
|
553
|
+
operation: 'get-commit-details',
|
|
554
|
+
operationType: 'READ',
|
|
555
|
+
componentType: 'Commit',
|
|
556
|
+
componentId: sha,
|
|
557
|
+
success: true,
|
|
558
|
+
parameters: { repoId, sha },
|
|
559
|
+
executionTimeMs: timer(),
|
|
560
|
+
});
|
|
561
|
+
return commit;
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
auditLogger.log({
|
|
565
|
+
operation: 'get-commit-details',
|
|
566
|
+
operationType: 'READ',
|
|
567
|
+
componentType: 'Commit',
|
|
568
|
+
componentId: sha,
|
|
569
|
+
success: false,
|
|
570
|
+
error: error.message,
|
|
571
|
+
parameters: { repoId, sha },
|
|
572
|
+
executionTimeMs: timer(),
|
|
573
|
+
});
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Search commits by message
|
|
579
|
+
*/
|
|
580
|
+
async searchCommits(query, repoId, author, since, until) {
|
|
581
|
+
const timer = auditLogger.startTimer();
|
|
582
|
+
try {
|
|
583
|
+
// Build search query
|
|
584
|
+
let searchQuery = query;
|
|
585
|
+
if (repoId) {
|
|
586
|
+
const repo = this.getRepoById(repoId);
|
|
587
|
+
searchQuery += ` repo:${repo.owner}/${repo.repo}`;
|
|
588
|
+
}
|
|
589
|
+
if (author) {
|
|
590
|
+
searchQuery += ` author:${author}`;
|
|
591
|
+
}
|
|
592
|
+
if (since) {
|
|
593
|
+
searchQuery += ` committer-date:>=${since}`;
|
|
594
|
+
}
|
|
595
|
+
if (until) {
|
|
596
|
+
searchQuery += ` committer-date:<=${until}`;
|
|
597
|
+
}
|
|
598
|
+
const result = await this.makeRequest(`search/commits?q=${encodeURIComponent(searchQuery)}`, { useCache: false } // Don't cache search results
|
|
599
|
+
);
|
|
600
|
+
auditLogger.log({
|
|
601
|
+
operation: 'search-commits',
|
|
602
|
+
operationType: 'READ',
|
|
603
|
+
componentType: 'Commit',
|
|
604
|
+
success: true,
|
|
605
|
+
parameters: { query, repoId, author, since, until, totalResults: result.total_count },
|
|
606
|
+
executionTimeMs: timer(),
|
|
607
|
+
});
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
auditLogger.log({
|
|
612
|
+
operation: 'search-commits',
|
|
613
|
+
operationType: 'READ',
|
|
614
|
+
componentType: 'Commit',
|
|
615
|
+
success: false,
|
|
616
|
+
error: error.message,
|
|
617
|
+
parameters: { query, repoId, author, since, until },
|
|
618
|
+
executionTimeMs: timer(),
|
|
619
|
+
});
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Compare two branches
|
|
625
|
+
*/
|
|
626
|
+
async compareBranches(repoId, base, head) {
|
|
627
|
+
const timer = auditLogger.startTimer();
|
|
628
|
+
const repo = this.getRepoById(repoId);
|
|
629
|
+
try {
|
|
630
|
+
const comparison = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/compare/${base}...${head}`, { repoId });
|
|
631
|
+
auditLogger.log({
|
|
632
|
+
operation: 'compare-branches',
|
|
633
|
+
operationType: 'READ',
|
|
634
|
+
componentType: 'Branch',
|
|
635
|
+
success: true,
|
|
636
|
+
parameters: { repoId, base, head, aheadBy: comparison.ahead_by, behindBy: comparison.behind_by },
|
|
637
|
+
executionTimeMs: timer(),
|
|
638
|
+
});
|
|
639
|
+
return comparison;
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
auditLogger.log({
|
|
643
|
+
operation: 'compare-branches',
|
|
644
|
+
operationType: 'READ',
|
|
645
|
+
componentType: 'Branch',
|
|
646
|
+
success: false,
|
|
647
|
+
error: error.message,
|
|
648
|
+
parameters: { repoId, base, head },
|
|
649
|
+
executionTimeMs: timer(),
|
|
650
|
+
});
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Get branch details
|
|
656
|
+
*/
|
|
657
|
+
async getBranchDetails(repoId, branch) {
|
|
658
|
+
const timer = auditLogger.startTimer();
|
|
659
|
+
const repo = this.getRepoById(repoId);
|
|
660
|
+
try {
|
|
661
|
+
const branchInfo = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/branches/${branch}`, { repoId });
|
|
662
|
+
auditLogger.log({
|
|
663
|
+
operation: 'get-branch-details',
|
|
664
|
+
operationType: 'READ',
|
|
665
|
+
componentType: 'Branch',
|
|
666
|
+
componentName: branch,
|
|
667
|
+
success: true,
|
|
668
|
+
parameters: { repoId, branch },
|
|
669
|
+
executionTimeMs: timer(),
|
|
670
|
+
});
|
|
671
|
+
return branchInfo;
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
auditLogger.log({
|
|
675
|
+
operation: 'get-branch-details',
|
|
676
|
+
operationType: 'READ',
|
|
677
|
+
componentType: 'Branch',
|
|
678
|
+
componentName: branch,
|
|
679
|
+
success: false,
|
|
680
|
+
error: error.message,
|
|
681
|
+
parameters: { repoId, branch },
|
|
682
|
+
executionTimeMs: timer(),
|
|
683
|
+
});
|
|
684
|
+
throw error;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* List pull requests
|
|
689
|
+
*/
|
|
690
|
+
async listPullRequests(repoId, state = 'open', base, head, sort = 'created', limit = 30) {
|
|
691
|
+
const timer = auditLogger.startTimer();
|
|
692
|
+
const repo = this.getRepoById(repoId);
|
|
693
|
+
try {
|
|
694
|
+
const params = {
|
|
695
|
+
state,
|
|
696
|
+
sort,
|
|
697
|
+
per_page: limit,
|
|
698
|
+
};
|
|
699
|
+
if (base)
|
|
700
|
+
params.base = base;
|
|
701
|
+
if (head)
|
|
702
|
+
params.head = head;
|
|
703
|
+
const queryString = new URLSearchParams(params).toString();
|
|
704
|
+
const prs = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/pulls?${queryString}`, { repoId });
|
|
705
|
+
auditLogger.log({
|
|
706
|
+
operation: 'list-pull-requests',
|
|
707
|
+
operationType: 'READ',
|
|
708
|
+
componentType: 'PullRequest',
|
|
709
|
+
success: true,
|
|
710
|
+
parameters: { repoId, state, base, head, sort, limit, count: prs.length },
|
|
711
|
+
executionTimeMs: timer(),
|
|
712
|
+
});
|
|
713
|
+
return prs;
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
auditLogger.log({
|
|
717
|
+
operation: 'list-pull-requests',
|
|
718
|
+
operationType: 'READ',
|
|
719
|
+
componentType: 'PullRequest',
|
|
720
|
+
success: false,
|
|
721
|
+
error: error.message,
|
|
722
|
+
parameters: { repoId, state, base, head, sort, limit },
|
|
723
|
+
executionTimeMs: timer(),
|
|
724
|
+
});
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Get pull request details
|
|
730
|
+
*/
|
|
731
|
+
async getPullRequest(repoId, prNumber) {
|
|
732
|
+
const timer = auditLogger.startTimer();
|
|
733
|
+
const repo = this.getRepoById(repoId);
|
|
734
|
+
try {
|
|
735
|
+
const pr = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/pulls/${prNumber}`, { repoId });
|
|
736
|
+
auditLogger.log({
|
|
737
|
+
operation: 'get-pull-request',
|
|
738
|
+
operationType: 'READ',
|
|
739
|
+
componentType: 'PullRequest',
|
|
740
|
+
componentId: prNumber.toString(),
|
|
741
|
+
success: true,
|
|
742
|
+
parameters: { repoId, prNumber },
|
|
743
|
+
executionTimeMs: timer(),
|
|
744
|
+
});
|
|
745
|
+
return pr;
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
auditLogger.log({
|
|
749
|
+
operation: 'get-pull-request',
|
|
750
|
+
operationType: 'READ',
|
|
751
|
+
componentType: 'PullRequest',
|
|
752
|
+
componentId: prNumber.toString(),
|
|
753
|
+
success: false,
|
|
754
|
+
error: error.message,
|
|
755
|
+
parameters: { repoId, prNumber },
|
|
756
|
+
executionTimeMs: timer(),
|
|
757
|
+
});
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Get pull request files
|
|
763
|
+
*/
|
|
764
|
+
async getPullRequestFiles(repoId, prNumber) {
|
|
765
|
+
const timer = auditLogger.startTimer();
|
|
766
|
+
const repo = this.getRepoById(repoId);
|
|
767
|
+
try {
|
|
768
|
+
const files = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/pulls/${prNumber}/files`, { repoId });
|
|
769
|
+
auditLogger.log({
|
|
770
|
+
operation: 'get-pr-files',
|
|
771
|
+
operationType: 'READ',
|
|
772
|
+
componentType: 'PullRequest',
|
|
773
|
+
componentId: prNumber.toString(),
|
|
774
|
+
success: true,
|
|
775
|
+
parameters: { repoId, prNumber, fileCount: files.length },
|
|
776
|
+
executionTimeMs: timer(),
|
|
777
|
+
});
|
|
778
|
+
return files;
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
auditLogger.log({
|
|
782
|
+
operation: 'get-pr-files',
|
|
783
|
+
operationType: 'READ',
|
|
784
|
+
componentType: 'PullRequest',
|
|
785
|
+
componentId: prNumber.toString(),
|
|
786
|
+
success: false,
|
|
787
|
+
error: error.message,
|
|
788
|
+
parameters: { repoId, prNumber },
|
|
789
|
+
executionTimeMs: timer(),
|
|
790
|
+
});
|
|
791
|
+
throw error;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Create a new branch (requires GHE_ENABLE_CREATE=true)
|
|
796
|
+
*/
|
|
797
|
+
async createBranch(repoId, branchName, fromBranch) {
|
|
798
|
+
if (!this.config.enableCreate) {
|
|
799
|
+
throw new Error('Branch creation is disabled. Set GHE_ENABLE_CREATE=true to enable.');
|
|
800
|
+
}
|
|
801
|
+
const timer = auditLogger.startTimer();
|
|
802
|
+
const repo = this.getRepoById(repoId);
|
|
803
|
+
try {
|
|
804
|
+
// Get source branch SHA
|
|
805
|
+
const sourceBranch = fromBranch || (await this.getDefaultBranch(repoId)).branch;
|
|
806
|
+
const branchInfo = await this.getBranchDetails(repoId, sourceBranch);
|
|
807
|
+
const sha = branchInfo.commit.sha;
|
|
808
|
+
// Create new branch
|
|
809
|
+
const result = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/git/refs`, {
|
|
810
|
+
method: 'POST',
|
|
811
|
+
data: {
|
|
812
|
+
ref: `refs/heads/${branchName}`,
|
|
813
|
+
sha,
|
|
814
|
+
},
|
|
815
|
+
useCache: false,
|
|
816
|
+
});
|
|
817
|
+
auditLogger.log({
|
|
818
|
+
operation: 'create-branch',
|
|
819
|
+
operationType: 'CREATE',
|
|
820
|
+
componentType: 'Branch',
|
|
821
|
+
componentName: branchName,
|
|
822
|
+
success: true,
|
|
823
|
+
parameters: { repoId, branchName, fromBranch: sourceBranch },
|
|
824
|
+
executionTimeMs: timer(),
|
|
825
|
+
});
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
auditLogger.log({
|
|
830
|
+
operation: 'create-branch',
|
|
831
|
+
operationType: 'CREATE',
|
|
832
|
+
componentType: 'Branch',
|
|
833
|
+
componentName: branchName,
|
|
834
|
+
success: false,
|
|
835
|
+
error: error.message,
|
|
836
|
+
parameters: { repoId, branchName, fromBranch },
|
|
837
|
+
executionTimeMs: timer(),
|
|
838
|
+
});
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Update file content (requires GHE_ENABLE_WRITE=true)
|
|
844
|
+
*/
|
|
845
|
+
async updateFile(repoId, path, content, message, branch, sha) {
|
|
846
|
+
if (!this.config.enableWrite) {
|
|
847
|
+
throw new Error('File updates are disabled. Set GHE_ENABLE_WRITE=true to enable.');
|
|
848
|
+
}
|
|
849
|
+
const timer = auditLogger.startTimer();
|
|
850
|
+
const repo = this.getRepoById(repoId);
|
|
851
|
+
try {
|
|
852
|
+
const encodedContent = Buffer.from(content).toString('base64');
|
|
853
|
+
const result = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${path}`, {
|
|
854
|
+
method: 'PUT',
|
|
855
|
+
data: {
|
|
856
|
+
message,
|
|
857
|
+
content: encodedContent,
|
|
858
|
+
sha,
|
|
859
|
+
branch,
|
|
860
|
+
},
|
|
861
|
+
useCache: false,
|
|
862
|
+
});
|
|
863
|
+
auditLogger.log({
|
|
864
|
+
operation: 'update-file',
|
|
865
|
+
operationType: 'UPDATE',
|
|
866
|
+
componentType: 'File',
|
|
867
|
+
componentName: path,
|
|
868
|
+
success: true,
|
|
869
|
+
parameters: { repoId, path, branch, message },
|
|
870
|
+
executionTimeMs: timer(),
|
|
871
|
+
});
|
|
872
|
+
return result;
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
auditLogger.log({
|
|
876
|
+
operation: 'update-file',
|
|
877
|
+
operationType: 'UPDATE',
|
|
878
|
+
componentType: 'File',
|
|
879
|
+
componentName: path,
|
|
880
|
+
success: false,
|
|
881
|
+
error: error.message,
|
|
882
|
+
parameters: { repoId, path, branch },
|
|
883
|
+
executionTimeMs: timer(),
|
|
884
|
+
});
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Create a new file (requires GHE_ENABLE_CREATE=true)
|
|
890
|
+
*/
|
|
891
|
+
async createFile(repoId, path, content, message, branch) {
|
|
892
|
+
if (!this.config.enableCreate) {
|
|
893
|
+
throw new Error('File creation is disabled. Set GHE_ENABLE_CREATE=true to enable.');
|
|
894
|
+
}
|
|
895
|
+
const timer = auditLogger.startTimer();
|
|
896
|
+
const repo = this.getRepoById(repoId);
|
|
897
|
+
try {
|
|
898
|
+
const encodedContent = Buffer.from(content).toString('base64');
|
|
899
|
+
const result = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${path}`, {
|
|
900
|
+
method: 'PUT',
|
|
901
|
+
data: {
|
|
902
|
+
message,
|
|
903
|
+
content: encodedContent,
|
|
904
|
+
branch,
|
|
905
|
+
},
|
|
906
|
+
useCache: false,
|
|
907
|
+
});
|
|
908
|
+
auditLogger.log({
|
|
909
|
+
operation: 'create-file',
|
|
910
|
+
operationType: 'CREATE',
|
|
911
|
+
componentType: 'File',
|
|
912
|
+
componentName: path,
|
|
913
|
+
success: true,
|
|
914
|
+
parameters: { repoId, path, branch, message },
|
|
915
|
+
executionTimeMs: timer(),
|
|
916
|
+
});
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
auditLogger.log({
|
|
921
|
+
operation: 'create-file',
|
|
922
|
+
operationType: 'CREATE',
|
|
923
|
+
componentType: 'File',
|
|
924
|
+
componentName: path,
|
|
925
|
+
success: false,
|
|
926
|
+
error: error.message,
|
|
927
|
+
parameters: { repoId, path, branch },
|
|
928
|
+
executionTimeMs: timer(),
|
|
929
|
+
});
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Search repositories
|
|
935
|
+
*/
|
|
936
|
+
async searchRepositories(query, owner) {
|
|
937
|
+
const timer = auditLogger.startTimer();
|
|
938
|
+
try {
|
|
939
|
+
let searchQuery = query;
|
|
940
|
+
if (owner) {
|
|
941
|
+
searchQuery += ` org:${owner}`;
|
|
942
|
+
}
|
|
943
|
+
const result = await this.makeRequest(`search/repositories?q=${encodeURIComponent(searchQuery)}`, { useCache: false });
|
|
944
|
+
auditLogger.log({
|
|
945
|
+
operation: 'search-repositories',
|
|
946
|
+
operationType: 'READ',
|
|
947
|
+
componentType: 'Repository',
|
|
948
|
+
success: true,
|
|
949
|
+
parameters: { query, owner, totalResults: result.total_count },
|
|
950
|
+
executionTimeMs: timer(),
|
|
951
|
+
});
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
auditLogger.log({
|
|
956
|
+
operation: 'search-repositories',
|
|
957
|
+
operationType: 'READ',
|
|
958
|
+
componentType: 'Repository',
|
|
959
|
+
success: false,
|
|
960
|
+
error: error.message,
|
|
961
|
+
parameters: { query, owner },
|
|
962
|
+
executionTimeMs: timer(),
|
|
963
|
+
});
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Get directory structure recursively
|
|
969
|
+
*/
|
|
970
|
+
async getDirectoryStructure(repoId, path, branch, depth = 3) {
|
|
971
|
+
const timer = auditLogger.startTimer();
|
|
972
|
+
const repo = this.getRepoById(repoId);
|
|
973
|
+
try {
|
|
974
|
+
// Auto-detect branch if not specified
|
|
975
|
+
const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch;
|
|
976
|
+
// Recursive function to build tree
|
|
977
|
+
const buildTree = async (currentPath, currentDepth) => {
|
|
978
|
+
if (currentDepth > depth) {
|
|
979
|
+
return { truncated: true };
|
|
980
|
+
}
|
|
981
|
+
const contents = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${currentPath}?ref=${selectedBranch}`, { repoId });
|
|
982
|
+
const tree = [];
|
|
983
|
+
for (const item of contents) {
|
|
984
|
+
if (item.type === 'dir' && currentDepth < depth) {
|
|
985
|
+
tree.push({
|
|
986
|
+
...item,
|
|
987
|
+
children: await buildTree(item.path, currentDepth + 1)
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
tree.push(item);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return tree;
|
|
995
|
+
};
|
|
996
|
+
const tree = await buildTree(path || '', 1);
|
|
997
|
+
auditLogger.log({
|
|
998
|
+
operation: 'get-directory-structure',
|
|
999
|
+
operationType: 'READ',
|
|
1000
|
+
componentType: 'Directory',
|
|
1001
|
+
componentName: path || '/',
|
|
1002
|
+
success: true,
|
|
1003
|
+
parameters: { repoId, path, branch: selectedBranch, depth },
|
|
1004
|
+
executionTimeMs: timer(),
|
|
1005
|
+
});
|
|
1006
|
+
return { tree, branch: selectedBranch };
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
auditLogger.log({
|
|
1010
|
+
operation: 'get-directory-structure',
|
|
1011
|
+
operationType: 'READ',
|
|
1012
|
+
componentType: 'Directory',
|
|
1013
|
+
componentName: path || '/',
|
|
1014
|
+
success: false,
|
|
1015
|
+
error: error.message,
|
|
1016
|
+
parameters: { repoId, path, branch, depth },
|
|
1017
|
+
executionTimeMs: timer(),
|
|
1018
|
+
});
|
|
1019
|
+
throw error;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Get file commit history
|
|
1024
|
+
*/
|
|
1025
|
+
async getFileHistory(repoId, path, branch, limit = 50) {
|
|
1026
|
+
const timer = auditLogger.startTimer();
|
|
1027
|
+
try {
|
|
1028
|
+
const commits = await this.getCommits(repoId, branch, undefined, undefined, undefined, path, limit);
|
|
1029
|
+
auditLogger.log({
|
|
1030
|
+
operation: 'get-file-history',
|
|
1031
|
+
operationType: 'READ',
|
|
1032
|
+
componentType: 'File',
|
|
1033
|
+
componentName: path,
|
|
1034
|
+
success: true,
|
|
1035
|
+
parameters: { repoId, path, branch, limit, count: commits.length },
|
|
1036
|
+
executionTimeMs: timer(),
|
|
1037
|
+
});
|
|
1038
|
+
return commits;
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
auditLogger.log({
|
|
1042
|
+
operation: 'get-file-history',
|
|
1043
|
+
operationType: 'READ',
|
|
1044
|
+
componentType: 'File',
|
|
1045
|
+
componentName: path,
|
|
1046
|
+
success: false,
|
|
1047
|
+
error: error.message,
|
|
1048
|
+
parameters: { repoId, path, branch, limit },
|
|
1049
|
+
executionTimeMs: timer(),
|
|
1050
|
+
});
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Get commit diff
|
|
1056
|
+
*/
|
|
1057
|
+
async getCommitDiff(repoId, sha, format = 'diff') {
|
|
1058
|
+
const timer = auditLogger.startTimer();
|
|
1059
|
+
const repo = this.getRepoById(repoId);
|
|
1060
|
+
try {
|
|
1061
|
+
const acceptHeader = format === 'patch'
|
|
1062
|
+
? 'application/vnd.github.v3.patch'
|
|
1063
|
+
: 'application/vnd.github.v3.diff';
|
|
1064
|
+
const token = await this.getAccessToken();
|
|
1065
|
+
const url = `${this.baseApiUrl}/repos/${repo.owner}/${repo.repo}/commits/${sha}`;
|
|
1066
|
+
const response = await axios({
|
|
1067
|
+
method: 'GET',
|
|
1068
|
+
url,
|
|
1069
|
+
headers: {
|
|
1070
|
+
'Authorization': `token ${token}`,
|
|
1071
|
+
'Accept': acceptHeader,
|
|
1072
|
+
'X-GitHub-Api-Version': this.config.apiVersion,
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
auditLogger.log({
|
|
1076
|
+
operation: 'get-commit-diff',
|
|
1077
|
+
operationType: 'READ',
|
|
1078
|
+
componentType: 'Commit',
|
|
1079
|
+
componentId: sha,
|
|
1080
|
+
success: true,
|
|
1081
|
+
parameters: { repoId, sha, format },
|
|
1082
|
+
executionTimeMs: timer(),
|
|
1083
|
+
});
|
|
1084
|
+
return response.data;
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
auditLogger.log({
|
|
1088
|
+
operation: 'get-commit-diff',
|
|
1089
|
+
operationType: 'READ',
|
|
1090
|
+
componentType: 'Commit',
|
|
1091
|
+
componentId: sha,
|
|
1092
|
+
success: false,
|
|
1093
|
+
error: error.message,
|
|
1094
|
+
parameters: { repoId, sha, format },
|
|
1095
|
+
executionTimeMs: timer(),
|
|
1096
|
+
});
|
|
1097
|
+
throw error;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
//# sourceMappingURL=GitHubEnterpriseService.js.map
|