Skip to main content
Glama
MIT License
27,120
19,780
  • Linux
  • Apple
gitCommand.test.ts14.6 kB
import { beforeEach, describe, expect, test, vi } from 'vitest'; import { execGitDiff, execGitLog, execGitLogFilenames, execGitRevParse, execGitShallowClone, execGitVersion, execLsRemote, } from '../../../src/core/git/gitCommand.js'; import { logger } from '../../../src/shared/logger.js'; vi.mock('../../../src/shared/logger'); describe('gitCommand', () => { beforeEach(() => { vi.resetAllMocks(); }); describe('execGitLogFilenames', () => { test('should return filenames from git log', async () => { const mockOutput = ` file1.ts file2.ts file1.ts file3.ts file2.ts `.trim(); const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: mockOutput }); const result = await execGitLogFilenames('/test/dir', 5, { execFileAsync: mockFileExecAsync }); expect(result).toEqual(['file1.ts', 'file2.ts', 'file1.ts', 'file3.ts', 'file2.ts']); expect(mockFileExecAsync).toHaveBeenCalledWith('git', [ '-C', '/test/dir', 'log', '--pretty=format:', '--name-only', '-n', '5', ]); }); test('should return empty array when git command fails', async () => { const mockFileExecAsync = vi.fn().mockRejectedValue(new Error('git command failed')); const result = await execGitLogFilenames('/test/dir', 5, { execFileAsync: mockFileExecAsync }); expect(result).toEqual([]); expect(logger.trace).toHaveBeenCalledWith('Failed to get git log filenames:', 'git command failed'); }); }); describe('execGitDiff', () => { test('should return git diff output', async () => { const mockDiff = 'diff --git a/file.txt b/file.txt\n+new line'; const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: mockDiff }); const result = await execGitDiff('/test/dir', [], { execFileAsync: mockFileExecAsync }); expect(result).toBe(mockDiff); expect(mockFileExecAsync).toHaveBeenCalledWith('git', ['-C', '/test/dir', 'diff', '--no-color']); }); test('should throw error when git diff fails', async () => { const mockFileExecAsync = vi.fn().mockRejectedValue(new Error('git command failed')); await expect(execGitDiff('/test/dir', [], { execFileAsync: mockFileExecAsync })).rejects.toThrow( 'git command failed', ); expect(logger.trace).toHaveBeenCalledWith('Failed to execute git diff:', 'git command failed'); }); }); describe('execGitVersion', () => { test('should return git version output', async () => { const mockVersion = 'git version 2.34.1'; const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: mockVersion }); const result = await execGitVersion({ execFileAsync: mockFileExecAsync }); expect(result).toBe(mockVersion); expect(mockFileExecAsync).toHaveBeenCalledWith('git', ['--version']); }); test('should throw error when git version fails', async () => { const mockFileExecAsync = vi.fn().mockRejectedValue(new Error('Command not found: git')); await expect(execGitVersion({ execFileAsync: mockFileExecAsync })).rejects.toThrow('Command not found: git'); expect(logger.trace).toHaveBeenCalledWith('Failed to execute git version:', 'Command not found: git'); }); }); describe('execGitRevParse', () => { test('should return git rev-parse output', async () => { const mockOutput = 'true'; const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: mockOutput }); const result = await execGitRevParse('/test/dir', { execFileAsync: mockFileExecAsync }); expect(result).toBe(mockOutput); expect(mockFileExecAsync).toHaveBeenCalledWith('git', ['-C', '/test/dir', 'rev-parse', '--is-inside-work-tree']); }); test('should throw error when git rev-parse fails', async () => { const mockFileExecAsync = vi.fn().mockRejectedValue(new Error('Not a git repository')); await expect(execGitRevParse('/test/dir', { execFileAsync: mockFileExecAsync })).rejects.toThrow( 'Not a git repository', ); expect(logger.trace).toHaveBeenCalledWith('Failed to execute git rev-parse:', 'Not a git repository'); }); }); describe('execGitShallowClone', () => { test('should execute without branch option if not specified by user', async () => { const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: '', stderr: '' }); const url = 'https://github.com/user/repo.git'; const directory = '/tmp/repo'; const remoteBranch = undefined; await execGitShallowClone(url, directory, remoteBranch, { execFileAsync: mockFileExecAsync }); expect(mockFileExecAsync).toHaveBeenCalledWith('git', ['clone', '--depth', '1', '--', url, directory]); }); test('should throw error when git clone fails', async () => { const mockFileExecAsync = vi.fn().mockRejectedValue(new Error('Authentication failed')); const url = 'https://github.com/user/repo.git'; const directory = '/tmp/repo'; const remoteBranch = undefined; await expect( execGitShallowClone(url, directory, remoteBranch, { execFileAsync: mockFileExecAsync }), ).rejects.toThrow('Authentication failed'); expect(mockFileExecAsync).toHaveBeenCalledWith('git', ['clone', '--depth', '1', '--', url, directory]); }); test('should execute commands correctly when branch is specified', async () => { const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: '', stderr: '' }); const url = 'https://github.com/user/repo.git'; const directory = '/tmp/repo'; const remoteBranch = 'main'; await execGitShallowClone(url, directory, remoteBranch, { execFileAsync: mockFileExecAsync }); expect(mockFileExecAsync).toHaveBeenCalledTimes(4); expect(mockFileExecAsync).toHaveBeenNthCalledWith(1, 'git', ['-C', directory, 'init']); expect(mockFileExecAsync).toHaveBeenNthCalledWith(2, 'git', [ '-C', directory, 'remote', 'add', '--', 'origin', url, ]); expect(mockFileExecAsync).toHaveBeenNthCalledWith(3, 'git', [ '-C', directory, 'fetch', '--depth', '1', 'origin', remoteBranch, ]); expect(mockFileExecAsync).toHaveBeenNthCalledWith(4, 'git', ['-C', directory, 'checkout', 'FETCH_HEAD']); }); test('should throw error when git fetch fails', async () => { const mockFileExecAsync = vi .fn() .mockResolvedValueOnce('Success on first call') .mockResolvedValueOnce('Success on second call') .mockRejectedValueOnce(new Error('Authentication failed')); const url = 'https://github.com/user/repo.git'; const directory = '/tmp/repo'; const remoteBranch = 'b188a6cb39b512a9c6da7235b880af42c78ccd0d'; await expect( execGitShallowClone(url, directory, remoteBranch, { execFileAsync: mockFileExecAsync }), ).rejects.toThrow('Authentication failed'); expect(mockFileExecAsync).toHaveBeenCalledTimes(3); expect(mockFileExecAsync).toHaveBeenNthCalledWith(1, 'git', ['-C', directory, 'init']); expect(mockFileExecAsync).toHaveBeenNthCalledWith(2, 'git', [ '-C', directory, 'remote', 'add', '--', 'origin', url, ]); expect(mockFileExecAsync).toHaveBeenLastCalledWith('git', [ '-C', directory, 'fetch', '--depth', '1', 'origin', remoteBranch, ]); }); test('should handle short SHA correctly', async () => { const url = 'https://github.com/user/repo.git'; const directory = '/tmp/repo'; const shortSha = 'ce9b621'; const mockFileExecAsync = vi .fn() .mockResolvedValueOnce('Success on first call') .mockResolvedValueOnce('Success on second call') .mockRejectedValueOnce( new Error( `Command failed: git fetch --depth 1 origin ${shortSha}\nfatal: couldn't find remote ref ${shortSha}`, ), ); await execGitShallowClone(url, directory, shortSha, { execFileAsync: mockFileExecAsync }); expect(mockFileExecAsync).toHaveBeenCalledTimes(5); expect(mockFileExecAsync).toHaveBeenNthCalledWith(1, 'git', ['-C', directory, 'init']); expect(mockFileExecAsync).toHaveBeenNthCalledWith(2, 'git', [ '-C', directory, 'remote', 'add', '--', 'origin', url, ]); expect(mockFileExecAsync).toHaveBeenNthCalledWith(3, 'git', [ '-C', directory, 'fetch', '--depth', '1', 'origin', shortSha, ]); expect(mockFileExecAsync).toHaveBeenNthCalledWith(4, 'git', ['-C', directory, 'fetch', 'origin']); expect(mockFileExecAsync).toHaveBeenLastCalledWith('git', ['-C', directory, 'checkout', shortSha]); }); test("should throw error when remote ref is not found, and it's not due to short SHA", async () => { const url = 'https://github.com/user/repo.git'; const directory = '/tmp/repo'; const remoteBranch = 'b188a6cb39b512a9c6da7235b880af42c78ccd0d'; const errMessage = `Command failed: git fetch --depth 1 origin ${remoteBranch}\nfatal: couldn't find remote ref ${remoteBranch}`; const mockFileExecAsync = vi .fn() .mockResolvedValueOnce('Success on first call') .mockResolvedValueOnce('Success on second call') .mockRejectedValueOnce(new Error(errMessage)); await expect( execGitShallowClone(url, directory, remoteBranch, { execFileAsync: mockFileExecAsync }), ).rejects.toThrow(errMessage); expect(mockFileExecAsync).toHaveBeenCalledTimes(3); expect(mockFileExecAsync).toHaveBeenNthCalledWith(1, 'git', ['-C', directory, 'init']); expect(mockFileExecAsync).toHaveBeenNthCalledWith(2, 'git', [ '-C', directory, 'remote', 'add', '--', 'origin', url, ]); expect(mockFileExecAsync).toHaveBeenLastCalledWith('git', [ '-C', directory, 'fetch', '--depth', '1', 'origin', remoteBranch, ]); }); }); describe('execGitLog', () => { test('should return git log with null character separator', async () => { const mockOutput = `\x002024-01-01 10:00:00 +0900|Initial commit file1.txt file2.txt \x002024-01-02 11:00:00 +0900|Add new feature src/feature.ts test/feature.test.ts`; const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: mockOutput }); const result = await execGitLog('/test/dir', 10, '%x00', { execFileAsync: mockFileExecAsync }); expect(result).toBe(mockOutput); expect(mockFileExecAsync).toHaveBeenCalledWith('git', [ '-C', '/test/dir', 'log', '--pretty=format:%x00%ad|%s', '--date=iso', '--name-only', '-n', '10', ]); }); test('should use custom record separator when provided', async () => { const customSeparator = '|SEPARATOR|'; const mockOutput = `${customSeparator}2024-01-01 10:00:00 +0900|Initial commit file1.txt`; const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: mockOutput }); const result = await execGitLog('/test/dir', 5, customSeparator, { execFileAsync: mockFileExecAsync }); expect(result).toBe(mockOutput); expect(mockFileExecAsync).toHaveBeenCalledWith('git', [ '-C', '/test/dir', 'log', `--pretty=format:${customSeparator}%ad|%s`, '--date=iso', '--name-only', '-n', '5', ]); }); test('should throw error when git log fails', async () => { const mockFileExecAsync = vi.fn().mockRejectedValue(new Error('git command failed')); await expect(execGitLog('/test/dir', 10, '%x00', { execFileAsync: mockFileExecAsync })).rejects.toThrow( 'git command failed', ); expect(logger.trace).toHaveBeenCalledWith('Failed to execute git log:', 'git command failed'); }); test('should work with different separators', async () => { const separator = '###'; const mockOutput = `${separator}2024-01-01 10:00:00 +0900|Test commit file.txt`; const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: mockOutput }); const result = await execGitLog('/test/dir', 50, separator, { execFileAsync: mockFileExecAsync }); expect(result).toBe(mockOutput); expect(mockFileExecAsync).toHaveBeenCalledWith('git', [ '-C', '/test/dir', 'log', `--pretty=format:${separator}%ad|%s`, '--date=iso', '--name-only', '-n', '50', ]); }); }); test('should reject URLs with dangerous parameters', async () => { const mockFileExecAsync = vi.fn(); const url = 'https://github.com/user/repo.git --upload-pack=evil-command'; const directory = '/tmp/repo'; const remoteBranch = undefined; await expect( execGitShallowClone(url, directory, remoteBranch, { execFileAsync: mockFileExecAsync }), ).rejects.toThrow('Invalid repository URL. URL contains potentially dangerous parameters'); expect(mockFileExecAsync).not.toHaveBeenCalled(); }); describe('execLsRemote', () => { test('should return git ls-remote output', async () => { const mockOutput = ` a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6\trefs/heads/main b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7\trefs/heads/develop c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8\trefs/tags/v1.0.0 `.trim(); const mockFileExecAsync = vi.fn().mockResolvedValue({ stdout: mockOutput }); const result = await execLsRemote('https://github.com/user/repo.git', { execFileAsync: mockFileExecAsync }); expect(result).toBe(mockOutput); expect(mockFileExecAsync).toHaveBeenCalledWith('git', [ 'ls-remote', '--heads', '--tags', '--', 'https://github.com/user/repo.git', ]); }); test('should throw error when git ls-remote fails', async () => { const mockFileExecAsync = vi.fn().mockRejectedValue(new Error('git command failed')); await expect( execLsRemote('https://github.com/user/repo.git', { execFileAsync: mockFileExecAsync }), ).rejects.toThrow('git command failed'); expect(logger.trace).toHaveBeenCalledWith('Failed to execute git ls-remote:', 'git command failed'); }); }); });

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/yamadashy/repomix'

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