gitingest-mcp
Version:
MCP server for transforming Git repositories into LLM-friendly text digests
168 lines • 5.83 kB
JavaScript
import { spawn } from "child_process";
import { promises as fs } from "fs";
import { tmpdir } from "os";
import { join } from "path";
export class GitCloneTool {
static async clone(options, signal) {
const { url, branch, commit, tag, depth = this.DEFAULT_DEPTH, sparse = false, subpath, includeSubmodules = false, } = options;
// Create temporary directory
const tempDir = await this.createTempDir();
try {
// Clone repository
const cloneArgs = ["clone"];
// Add branch/tag/commit
if (branch) {
cloneArgs.push("--branch", branch);
}
else if (tag) {
cloneArgs.push("--branch", tag);
}
// Add depth
if (depth && depth > 0 && depth <= this.MAX_DEPTH) {
cloneArgs.push("--depth", depth.toString());
}
// Add sparse checkout
if (sparse && subpath) {
cloneArgs.push("--filter=blob:none", "--sparse");
}
// Add submodules
if (includeSubmodules) {
cloneArgs.push("--recurse-submodules");
}
cloneArgs.push(url, tempDir);
await this.executeGitCommand(cloneArgs);
// If specific commit is provided, checkout it
if (commit) {
await this.executeGitCommand(["checkout", commit], { cwd: tempDir });
}
// If sparse checkout with subpath
if (sparse && subpath) {
await this.executeGitCommand(["sparse-checkout", "set", subpath], { cwd: tempDir });
}
// Get actual branch and commit
const branchName = await this.getCurrentBranch(tempDir);
const commitHash = await this.getCurrentCommit(tempDir);
return {
path: tempDir,
branch: branchName,
commit: commitHash,
isShallow: depth !== undefined && depth > 0,
};
}
catch (error) {
// Clean up on error
await this.cleanup(tempDir);
throw error;
}
}
static async getCurrentBranch(repoPath) {
try {
const output = await this.executeGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoPath });
return output.trim();
}
catch {
return "main";
}
}
static async getCurrentCommit(repoPath) {
try {
const output = await this.executeGitCommand(["rev-parse", "HEAD"], { cwd: repoPath });
return output.trim();
}
catch {
return "unknown";
}
}
static async getBranches(repoPath, remote = true) {
try {
const args = remote ? ["branch", "-r"] : ["branch", "-l"];
const output = await this.executeGitCommand(args, { cwd: repoPath });
return output
.split("\n")
.map(branch => branch.trim().replace(/^origin\//, ""))
.filter(branch => branch && !branch.includes("HEAD"));
}
catch {
return [];
}
}
static async getTags(repoPath) {
try {
const output = await this.executeGitCommand(["tag", "-l"], { cwd: repoPath });
return output.split("\n").filter(tag => tag.trim());
}
catch {
return [];
}
}
static async getCommits(repoPath, maxCount = 10) {
try {
const format = "%H|%s|%an|%ad";
const output = await this.executeGitCommand(["log", `--format=${format}`, `--max-count=${maxCount}`], { cwd: repoPath });
return output
.split("\n")
.filter(line => line.trim())
.map(line => {
const [hash, message, author, date] = line.split("|");
return { hash, message, author, date };
});
}
catch {
return [];
}
}
static async createTempDir() {
const tempBase = tmpdir();
const tempDir = join(tempBase, `gitingest-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
await fs.mkdir(tempDir, { recursive: true });
return tempDir;
}
static async executeGitCommand(args, options = {}) {
return new Promise((resolve, reject) => {
const git = spawn("git", args, {
cwd: options.cwd,
stdio: ["ignore", "pipe", "pipe"],
signal: options.signal,
});
let stdout = "";
let stderr = "";
git.stdout.on("data", (data) => {
stdout += data.toString();
});
git.stderr.on("data", (data) => {
stderr += data.toString();
});
git.on("close", (code) => {
if (code === 0) {
resolve(stdout);
}
else {
reject(new Error(`Git command failed: ${stderr || stdout}`));
}
});
git.on("error", (error) => {
reject(new Error(`Failed to execute git: ${error.message}`));
});
});
}
static async cleanup(repoPath) {
try {
await fs.rm(repoPath, { recursive: true, force: true });
}
catch {
// Ignore cleanup errors
}
}
static async isGitAvailable() {
try {
await this.executeGitCommand(["--version"]);
return true;
}
catch {
return false;
}
}
}
GitCloneTool.DEFAULT_DEPTH = 1;
GitCloneTool.MAX_DEPTH = 1000;
//# sourceMappingURL=git-clone.js.map