File size: 3,452 Bytes
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { config } from '../config.js';
import { FileInfo, FilesResponse } from '../types/contract.js';
import { isBinaryOrIgnoredFile } from '../utils/binary-filter.js';
import { logger } from '../utils/logger.js';
import { resolveMimeType } from '../utils/mime.js';
import { githubService } from './github-service.js';
import { treeCache } from './tree-cache.js';

export interface ResolvedTreeResult extends FilesResponse {
  pathMap: Map<string, string>; // lowercase -> exact path
}

export class ResolverService {
  async getOrDiscoverFiles(
    owner: string,
    repo: string,
    branch?: string,
    fresh = false,
    prefix?: string
  ): Promise<ResolvedTreeResult> {
    const startTime = performance.now();

    // 1. Resolve default branch if not provided
    let targetBranch = branch;
    if (!targetBranch) {
      const meta = await githubService.getRepoMetadata(owner, repo);
      targetBranch = meta.defaultBranch;
    }

    // 2. Check in-memory cache if fresh is false
    if (!fresh) {
      const cached = treeCache.get(owner, repo, targetBranch);
      if (cached) {
        let files = cached.files;
        if (prefix) {
          const normalizedPrefix = prefix.replace(/^\/+/, '');
          files = files.filter((f) => f.path.startsWith(normalizedPrefix));
        }
        return {
          repository: `${owner}/${repo}`,
          branch: targetBranch,
          totalFiles: files.length,
          files,
          pathMap: cached.pathMap,
        };
      }
    }

    // 3. Live discovery from GitHub
    const rawTree = await githubService.getGitTree(owner, repo, targetBranch);
    const pathMap = new Map<string, string>();
    const discoveredFiles: FileInfo[] = [];
    let skippedCount = 0;

    for (const item of rawTree) {
      if (item.type !== 'blob') {
        continue;
      }

      if (isBinaryOrIgnoredFile(item.path)) {
        skippedCount++;
        continue;
      }

      const mimeType = resolveMimeType(item.path);
      const fileInfo: FileInfo = {
        path: item.path,
        type: 'file',
        sizeBytes: item.size || 0,
        mimeType,
      };

      discoveredFiles.push(fileInfo);
      pathMap.set(item.path.toLowerCase(), item.path);
    }

    // 4. Save to cache
    treeCache.set(owner, repo, targetBranch, discoveredFiles, pathMap, config.cacheTtlMs);

    const durationMs = Math.round(performance.now() - startTime);
    logger.tree(discoveredFiles.length, durationMs, skippedCount);

    // 5. Apply prefix filter if requested
    let files = discoveredFiles;
    if (prefix) {
      const normalizedPrefix = prefix.replace(/^\/+/, '');
      files = files.filter((f) => f.path.startsWith(normalizedPrefix));
    }

    return {
      repository: `${owner}/${repo}`,
      branch: targetBranch,
      totalFiles: files.length,
      files,
      pathMap,
    };
  }

  async resolveFilePath(
    owner: string,
    repo: string,
    branch: string,
    requestedPath: string
  ): Promise<{ exactPath: string; branch: string }> {
    const cleanPath = requestedPath.replace(/^\/+/, '');
    const tree = await this.getOrDiscoverFiles(owner, repo, branch, false);

    // Exact or case-insensitive match
    const lowercaseKey = cleanPath.toLowerCase();
    const matchedPath = tree.pathMap.get(lowercaseKey);

    return {
      exactPath: matchedPath || cleanPath,
      branch: tree.branch,
    };
  }
}

export const resolverService = new ResolverService();