@mhalder/qdrant-mcp-server 2.2.0 → 3.1.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/.github/workflows/ci.yml +4 -1
- package/CHANGELOG.md +22 -0
- package/README.md +78 -3
- package/build/code/indexer.d.ts +2 -1
- package/build/code/indexer.d.ts.map +1 -1
- package/build/code/indexer.js +37 -7
- package/build/code/indexer.js.map +1 -1
- package/build/git/chunker.d.ts +39 -0
- package/build/git/chunker.d.ts.map +1 -0
- package/build/git/chunker.js +210 -0
- package/build/git/chunker.js.map +1 -0
- package/build/git/chunker.test.d.ts +2 -0
- package/build/git/chunker.test.d.ts.map +1 -0
- package/build/git/chunker.test.js +230 -0
- package/build/git/chunker.test.js.map +1 -0
- package/build/git/config.d.ts +34 -0
- package/build/git/config.d.ts.map +1 -0
- package/build/git/config.js +163 -0
- package/build/git/config.js.map +1 -0
- package/build/git/extractor.d.ts +57 -0
- package/build/git/extractor.d.ts.map +1 -0
- package/build/git/extractor.integration.test.d.ts +6 -0
- package/build/git/extractor.integration.test.d.ts.map +1 -0
- package/build/git/extractor.integration.test.js +166 -0
- package/build/git/extractor.integration.test.js.map +1 -0
- package/build/git/extractor.js +231 -0
- package/build/git/extractor.js.map +1 -0
- package/build/git/extractor.test.d.ts +2 -0
- package/build/git/extractor.test.d.ts.map +1 -0
- package/build/git/extractor.test.js +267 -0
- package/build/git/extractor.test.js.map +1 -0
- package/build/git/index.d.ts +10 -0
- package/build/git/index.d.ts.map +1 -0
- package/build/git/index.js +11 -0
- package/build/git/index.js.map +1 -0
- package/build/git/indexer.d.ts +50 -0
- package/build/git/indexer.d.ts.map +1 -0
- package/build/git/indexer.js +588 -0
- package/build/git/indexer.js.map +1 -0
- package/build/git/indexer.test.d.ts +2 -0
- package/build/git/indexer.test.d.ts.map +1 -0
- package/build/git/indexer.test.js +867 -0
- package/build/git/indexer.test.js.map +1 -0
- package/build/git/sync/synchronizer.d.ts +43 -0
- package/build/git/sync/synchronizer.d.ts.map +1 -0
- package/build/git/sync/synchronizer.js +108 -0
- package/build/git/sync/synchronizer.js.map +1 -0
- package/build/git/sync/synchronizer.test.d.ts +2 -0
- package/build/git/sync/synchronizer.test.d.ts.map +1 -0
- package/build/git/sync/synchronizer.test.js +188 -0
- package/build/git/sync/synchronizer.test.js.map +1 -0
- package/build/git/types.d.ts +159 -0
- package/build/git/types.d.ts.map +1 -0
- package/build/git/types.js +5 -0
- package/build/git/types.js.map +1 -0
- package/build/index.js +18 -0
- package/build/index.js.map +1 -1
- package/build/tools/git-history.d.ts +10 -0
- package/build/tools/git-history.d.ts.map +1 -0
- package/build/tools/git-history.js +144 -0
- package/build/tools/git-history.js.map +1 -0
- package/build/tools/index.d.ts +2 -0
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +4 -0
- package/build/tools/index.js.map +1 -1
- package/build/tools/schemas.d.ts +24 -0
- package/build/tools/schemas.d.ts.map +1 -1
- package/build/tools/schemas.js +64 -0
- package/build/tools/schemas.js.map +1 -1
- package/package.json +2 -2
- package/src/code/indexer.ts +49 -7
- package/src/git/chunker.test.ts +284 -0
- package/src/git/chunker.ts +256 -0
- package/src/git/config.ts +173 -0
- package/src/git/extractor.integration.test.ts +221 -0
- package/src/git/extractor.test.ts +403 -0
- package/src/git/extractor.ts +284 -0
- package/src/git/index.ts +31 -0
- package/src/git/indexer.test.ts +1089 -0
- package/src/git/indexer.ts +745 -0
- package/src/git/sync/synchronizer.test.ts +250 -0
- package/src/git/sync/synchronizer.ts +122 -0
- package/src/git/types.ts +192 -0
- package/src/index.ts +42 -0
- package/src/tools/git-history.ts +208 -0
- package/src/tools/index.ts +7 -0
- package/src/tools/schemas.ts +75 -0
- package/vitest.config.ts +2 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitExtractor - Extract commit data from git repositories
|
|
3
|
+
* Uses child_process.execFile for security (no shell injection)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import {
|
|
9
|
+
GIT_LOG_COMMIT_DELIMITER,
|
|
10
|
+
GIT_LOG_FORMAT,
|
|
11
|
+
GIT_MAX_BUFFER,
|
|
12
|
+
} from "./config.js";
|
|
13
|
+
import type { GitConfig, GitExtractOptions, RawCommit } from "./types.js";
|
|
14
|
+
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize git remote URL to consistent format for hashing.
|
|
19
|
+
* Handles both SSH and HTTPS URL formats.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* normalizeRemoteUrl("git@github.com:user/repo.git") // → "user/repo"
|
|
23
|
+
* normalizeRemoteUrl("https://github.com/user/repo.git") // → "user/repo"
|
|
24
|
+
* normalizeRemoteUrl("") // → ""
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeRemoteUrl(url: string): string {
|
|
27
|
+
if (!url) return "";
|
|
28
|
+
return url
|
|
29
|
+
.replace(/^git@[^:]+:/, "") // git@github.com:user/repo → user/repo
|
|
30
|
+
.replace(/^https?:\/\/[^/]+\//, "") // https://github.com/user/repo → user/repo
|
|
31
|
+
.replace(/\.git$/, ""); // user/repo.git → user/repo
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class GitExtractor {
|
|
35
|
+
constructor(
|
|
36
|
+
private repoPath: string,
|
|
37
|
+
private config: GitConfig,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validate that the path is a git repository
|
|
42
|
+
*/
|
|
43
|
+
async validateRepository(): Promise<boolean> {
|
|
44
|
+
try {
|
|
45
|
+
await execFileAsync("git", ["rev-parse", "--git-dir"], {
|
|
46
|
+
cwd: this.repoPath,
|
|
47
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
48
|
+
timeout: this.config.gitTimeout,
|
|
49
|
+
});
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the latest commit hash
|
|
58
|
+
*/
|
|
59
|
+
async getLatestCommitHash(): Promise<string> {
|
|
60
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
|
|
61
|
+
cwd: this.repoPath,
|
|
62
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
63
|
+
timeout: this.config.gitTimeout,
|
|
64
|
+
});
|
|
65
|
+
return stdout.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the remote origin URL, or empty string if not configured
|
|
70
|
+
*/
|
|
71
|
+
async getRemoteUrl(): Promise<string> {
|
|
72
|
+
try {
|
|
73
|
+
const { stdout } = await execFileAsync(
|
|
74
|
+
"git",
|
|
75
|
+
["remote", "get-url", "origin"],
|
|
76
|
+
{
|
|
77
|
+
cwd: this.repoPath,
|
|
78
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
79
|
+
timeout: this.config.gitTimeout,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
return stdout.trim();
|
|
83
|
+
} catch {
|
|
84
|
+
return ""; // No remote configured
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get total commit count (optionally after a specific commit)
|
|
90
|
+
*/
|
|
91
|
+
async getCommitCount(sinceCommit?: string): Promise<number> {
|
|
92
|
+
const args = ["rev-list", "--count"];
|
|
93
|
+
|
|
94
|
+
if (sinceCommit) {
|
|
95
|
+
args.push(`${sinceCommit}..HEAD`);
|
|
96
|
+
} else {
|
|
97
|
+
args.push("HEAD");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
101
|
+
cwd: this.repoPath,
|
|
102
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
103
|
+
timeout: this.config.gitTimeout,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return parseInt(stdout.trim(), 10);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract commits from the repository
|
|
111
|
+
*/
|
|
112
|
+
async getCommits(options?: GitExtractOptions): Promise<RawCommit[]> {
|
|
113
|
+
const maxCommits = options?.maxCommits ?? this.config.maxCommits;
|
|
114
|
+
|
|
115
|
+
// Build git log arguments
|
|
116
|
+
const args = [
|
|
117
|
+
"log",
|
|
118
|
+
`--pretty=format:${GIT_LOG_COMMIT_DELIMITER}${GIT_LOG_FORMAT}`,
|
|
119
|
+
"--numstat", // Include insertions/deletions per file
|
|
120
|
+
`-n${maxCommits}`,
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
// Add range if sinceCommit is specified
|
|
124
|
+
if (options?.sinceCommit) {
|
|
125
|
+
args.push(`${options.sinceCommit}..HEAD`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Add date filter if sinceDate is specified
|
|
129
|
+
if (options?.sinceDate) {
|
|
130
|
+
args.push(`--since=${options.sinceDate}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
134
|
+
cwd: this.repoPath,
|
|
135
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
136
|
+
timeout: this.config.gitTimeout,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return this.parseGitLog(stdout);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the diff for a specific commit
|
|
144
|
+
*/
|
|
145
|
+
async getCommitDiff(commitHash: string): Promise<string> {
|
|
146
|
+
try {
|
|
147
|
+
const { stdout } = await execFileAsync(
|
|
148
|
+
"git",
|
|
149
|
+
["show", "--no-color", "-p", commitHash],
|
|
150
|
+
{
|
|
151
|
+
cwd: this.repoPath,
|
|
152
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
153
|
+
timeout: this.config.gitTimeout,
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Truncate diff if it exceeds maxDiffSize
|
|
158
|
+
if (stdout.length > this.config.maxDiffSize) {
|
|
159
|
+
return (
|
|
160
|
+
stdout.substring(0, this.config.maxDiffSize) +
|
|
161
|
+
`\n\n[diff truncated: showing ${this.config.maxDiffSize} of ${stdout.length} bytes]`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return stdout;
|
|
166
|
+
} catch {
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse git log output into structured commits
|
|
173
|
+
*/
|
|
174
|
+
private parseGitLog(output: string): RawCommit[] {
|
|
175
|
+
const commits: RawCommit[] = [];
|
|
176
|
+
|
|
177
|
+
// Split by commit delimiter
|
|
178
|
+
const commitBlocks = output.split(GIT_LOG_COMMIT_DELIMITER);
|
|
179
|
+
|
|
180
|
+
for (const block of commitBlocks) {
|
|
181
|
+
const trimmed = block.trim();
|
|
182
|
+
if (!trimmed) continue;
|
|
183
|
+
|
|
184
|
+
const commit = this.parseCommitBlock(trimmed);
|
|
185
|
+
if (commit) {
|
|
186
|
+
commits.push(commit);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return commits;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Parse a single commit block
|
|
195
|
+
*/
|
|
196
|
+
private parseCommitBlock(block: string): RawCommit | null {
|
|
197
|
+
// The format line is first, followed by numstat output
|
|
198
|
+
const lines = block.split("\n");
|
|
199
|
+
if (lines.length === 0) return null;
|
|
200
|
+
|
|
201
|
+
// Parse the format line: hash|shortHash|author|authorEmail|date|subject|body
|
|
202
|
+
const formatLine = lines[0];
|
|
203
|
+
const parts = formatLine.split("|");
|
|
204
|
+
|
|
205
|
+
if (parts.length < 6) return null;
|
|
206
|
+
|
|
207
|
+
const [
|
|
208
|
+
hash,
|
|
209
|
+
shortHash,
|
|
210
|
+
author,
|
|
211
|
+
authorEmail,
|
|
212
|
+
dateStr,
|
|
213
|
+
subject,
|
|
214
|
+
...bodyParts
|
|
215
|
+
] = parts;
|
|
216
|
+
|
|
217
|
+
// Parse files and stats from numstat output
|
|
218
|
+
const { files, insertions, deletions } = this.parseNumstat(lines.slice(1));
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
hash,
|
|
222
|
+
shortHash,
|
|
223
|
+
author,
|
|
224
|
+
authorEmail,
|
|
225
|
+
date: new Date(dateStr),
|
|
226
|
+
subject,
|
|
227
|
+
body: bodyParts.join("|").trim(), // Body might contain | characters
|
|
228
|
+
files,
|
|
229
|
+
insertions,
|
|
230
|
+
deletions,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Parse numstat output (lines after the format line)
|
|
236
|
+
*/
|
|
237
|
+
private parseNumstat(lines: string[]): {
|
|
238
|
+
files: string[];
|
|
239
|
+
insertions: number;
|
|
240
|
+
deletions: number;
|
|
241
|
+
} {
|
|
242
|
+
const files: string[] = [];
|
|
243
|
+
let insertions = 0;
|
|
244
|
+
let deletions = 0;
|
|
245
|
+
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
const trimmed = line.trim();
|
|
248
|
+
if (!trimmed) continue;
|
|
249
|
+
|
|
250
|
+
// numstat format: insertions<tab>deletions<tab>filename
|
|
251
|
+
// Binary files show as "-" for insertions/deletions
|
|
252
|
+
const match = trimmed.match(/^(\d+|-)\s+(\d+|-)\s+(.+)$/);
|
|
253
|
+
|
|
254
|
+
if (match) {
|
|
255
|
+
const [, ins, del, filename] = match;
|
|
256
|
+
|
|
257
|
+
// Handle binary files (marked with -)
|
|
258
|
+
if (ins !== "-") {
|
|
259
|
+
insertions += parseInt(ins, 10);
|
|
260
|
+
}
|
|
261
|
+
if (del !== "-") {
|
|
262
|
+
deletions += parseInt(del, 10);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Handle renamed files (old -> new)
|
|
266
|
+
if (filename.includes(" => ")) {
|
|
267
|
+
const renameParts = filename.match(/(.+)\{(.+) => (.+)\}(.+)?/);
|
|
268
|
+
if (renameParts) {
|
|
269
|
+
files.push(
|
|
270
|
+
`${renameParts[1]}${renameParts[3]}${renameParts[4] || ""}`,
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
const simpleRename = filename.split(" => ");
|
|
274
|
+
files.push(simpleRename[1] || filename);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
files.push(filename);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { files, insertions, deletions };
|
|
283
|
+
}
|
|
284
|
+
}
|
package/src/git/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git history indexing module - barrel export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Types
|
|
6
|
+
export type {
|
|
7
|
+
CommitChunk,
|
|
8
|
+
CommitType,
|
|
9
|
+
GitChangeStats,
|
|
10
|
+
GitConfig,
|
|
11
|
+
GitExtractOptions,
|
|
12
|
+
GitIndexOptions,
|
|
13
|
+
GitIndexStats,
|
|
14
|
+
GitIndexStatus,
|
|
15
|
+
GitIndexingStatus,
|
|
16
|
+
GitProgressCallback,
|
|
17
|
+
GitProgressUpdate,
|
|
18
|
+
GitSearchOptions,
|
|
19
|
+
GitSearchResult,
|
|
20
|
+
GitSnapshot,
|
|
21
|
+
RawCommit,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
|
|
24
|
+
// Config
|
|
25
|
+
export { DEFAULT_GIT_CONFIG, COMMIT_TYPE_PATTERNS } from "./config.js";
|
|
26
|
+
|
|
27
|
+
// Main classes
|
|
28
|
+
export { GitHistoryIndexer } from "./indexer.js";
|
|
29
|
+
export { GitExtractor } from "./extractor.js";
|
|
30
|
+
export { CommitChunker } from "./chunker.js";
|
|
31
|
+
export { GitSynchronizer } from "./sync/synchronizer.js";
|