Skip to main content
Glama
git-memory.ts8.94 kB
/** * In-Memory Git Utility * * This file contains utility functions for working with git repositories in memory * using isomorphic-git and memfs. This allows for git operations without writing * to the file system. */ import * as git from 'isomorphic-git'; import { fs as memfs } from 'memfs'; import { Dirent } from 'fs'; import http from 'isomorphic-git/http/web/index.cjs'; /** * Repository commit information */ export interface CommitInfo { sha: string; message: string; author: { name: string; email: string; timestamp: number; }; files: { path: string; type: 'add' | 'modify' | 'delete'; }[]; } /** * In-memory git repository */ export class InMemoryGit { private fs = memfs.promises; private dir: string; /** * Create a new in-memory git repository * @param {string} dir - The directory path in the virtual file system */ constructor(dir: string = '/repo') { this.dir = dir; } /** * Initialize a new git repository * @returns {Promise<void>} */ async init(): Promise<void> { // Create the directory if it doesn't exist await this.fs.mkdir(this.dir, { recursive: true }); // Initialize the repository await git.init({ fs: memfs, dir: this.dir }); } /** * Clone a repository from a URL * @param {string} url - The URL of the repository to clone * @param {string} token - GitHub token for authentication (optional) * @returns {Promise<void>} */ async clone(url: string, token?: string): Promise<void> { const headers = token ? { Authorization: `token ${token}` } : undefined; await git.clone({ fs: memfs, http, dir: this.dir, url, singleBranch: true, depth: 10, // Limit to 10 commits as requested headers }); } /** * Get the list of commits in the repository * @param {number} limit - Maximum number of commits to return * @returns {Promise<CommitInfo[]>} The list of commits */ async getCommits(limit: number = 10): Promise<CommitInfo[]> { // Get the current branch const currentBranch = await git.currentBranch({ fs: memfs, dir: this.dir }); if (!currentBranch) { throw new Error('No current branch found'); } // Get the commit log const commits = await git.log({ fs: memfs, dir: this.dir, depth: limit }); // Transform the commits to our format return Promise.all(commits.map(async commit => { // If this is not the first commit, get the diff with the previous commit const files: Array<{ path: string; type: 'add' | 'modify' | 'delete' }> = []; // Check if the commit has parents const parents = commit.commit.parent || []; if (parents.length > 0) { const diff = await git.walk({ fs: memfs, dir: this.dir, trees: [ git.TREE({ ref: parents[0] }), git.TREE({ ref: commit.oid }) ], map: async (filepath: string, [a, b]: any[]) => { if (!a && b) { files.push({ path: filepath, type: 'add' }); } else if (a && !b) { files.push({ path: filepath, type: 'delete' }); } else if (a && b && a.oid !== b.oid) { files.push({ path: filepath, type: 'modify' }); } return null; } }); } return { sha: commit.oid, message: commit.commit.message, author: { name: commit.commit.author.name, email: commit.commit.author.email, timestamp: commit.commit.author.timestamp }, files }; })); } /** * Get the content of a file at a specific commit * @param {string} filepath - The path of the file * @param {string} commitSha - The commit SHA (optional, defaults to HEAD) * @returns {Promise<string>} The file content */ async getFileAtCommit(filepath: string, commitSha?: string): Promise<string> { const ref = commitSha || 'HEAD'; try { const { blob } = await git.readBlob({ fs: memfs, dir: this.dir, oid: ref, filepath }); return new TextDecoder().decode(blob); } catch (error) { throw new Error(`File not found at commit ${ref}: ${filepath}`); } } /** * Get the list of files in the repository at a specific commit * @param {string} commitSha - The commit SHA (optional, defaults to HEAD) * @returns {Promise<string[]>} The list of file paths */ async getFilesAtCommit(commitSha?: string): Promise<string[]> { const ref = commitSha || 'HEAD'; const files: string[] = []; await git.walk({ fs: memfs, dir: this.dir, trees: [git.TREE({ ref })], map: async (filepath: string, [entry]: any[]) => { if (entry && typeof entry.type === 'string' && entry.type === 'blob') { files.push(filepath); } return null; } }); return files; } /** * Get the diff between two commits * @param {string} oldCommitSha - The old commit SHA * @param {string} newCommitSha - The new commit SHA * @returns {Promise<{path: string, type: 'add' | 'modify' | 'delete', oldContent?: string, newContent?: string}[]>} The diff */ async getDiff(oldCommitSha: string, newCommitSha: string): Promise<{ path: string; type: 'add' | 'modify' | 'delete'; oldContent?: string; newContent?: string; }[]> { const diff: { path: string; type: 'add' | 'modify' | 'delete'; oldContent?: string; newContent?: string; }[] = []; await git.walk({ fs: memfs, dir: this.dir, trees: [ git.TREE({ ref: oldCommitSha }), git.TREE({ ref: newCommitSha }) ], map: async (filepath: string, [a, b]: any[]) => { if (!a && b) { // File added const newContent = await this.getFileAtCommit(filepath, newCommitSha); diff.push({ path: filepath, type: 'add', newContent }); } else if (a && !b) { // File deleted const oldContent = await this.getFileAtCommit(filepath, oldCommitSha); diff.push({ path: filepath, type: 'delete', oldContent }); } else if (a && b && a.oid !== b.oid) { // File modified const oldContent = await this.getFileAtCommit(filepath, oldCommitSha); const newContent = await this.getFileAtCommit(filepath, newCommitSha); diff.push({ path: filepath, type: 'modify', oldContent, newContent }); } return null; } }); return diff; } /** * Get the evolution of a file across commits * @param {string} filepath - The path of the file * @param {number} limit - Maximum number of commits to analyze * @returns {Promise<{sha: string, message: string, content: string}[]>} The file evolution */ async getFileEvolution(filepath: string, limit: number = 10): Promise<{ sha: string; message: string; content: string; }[]> { const commits = await this.getCommits(limit); const evolution: { sha: string; message: string; content: string }[] = []; for (const commit of commits) { try { const content = await this.getFileAtCommit(filepath, commit.sha); evolution.push({ sha: commit.sha, message: commit.message, content }); } catch (error) { // File doesn't exist at this commit, skip } } return evolution; } /** * Get the structure of the repository at a specific commit * @param {string} commitSha - The commit SHA (optional, defaults to HEAD) * @returns {Promise<{path: string, type: 'file' | 'directory'}[]>} The repository structure */ async getRepoStructure(commitSha?: string): Promise<{ path: string; type: 'file' | 'directory'; }[]> { const ref = commitSha || 'HEAD'; const structure: { path: string; type: 'file' | 'directory' }[] = []; const directories = new Set<string>(); await git.walk({ fs: memfs, dir: this.dir, trees: [git.TREE({ ref })], map: async (filepath: string, [entry]: any[]) => { if (entry) { if (typeof entry.type === 'string' && entry.type === 'blob') { structure.push({ path: filepath, type: 'file' }); // Add all parent directories let dirPath = filepath; while ((dirPath = dirPath.substring(0, dirPath.lastIndexOf('/'))) !== '') { directories.add(dirPath); } } } return null; } }); // Add directories to the structure for (const dir of directories) { structure.push({ path: dir, type: 'directory' }); } return structure; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/phxdev1/archy-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server