mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-11 22:51:26 +00:00
feat: implement git-based lastUpdated dates
This commit is contained in:
parent
3e8d35b490
commit
4891fb04b7
3 changed files with 205 additions and 3 deletions
|
|
@ -2,7 +2,7 @@ import * as vscode from "vscode"
|
|||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import * as yaml from "js-yaml"
|
||||
import simpleGit from "simple-git"
|
||||
import simpleGit, { SimpleGit } from "simple-git"
|
||||
import { MetadataScanner } from "./MetadataScanner"
|
||||
import { validateAnyMetadata } from "./schemas"
|
||||
import { PackageManagerItem, PackageManagerRepository, RepositoryMetadata } from "./types"
|
||||
|
|
@ -12,13 +12,24 @@ import { PackageManagerItem, PackageManagerRepository, RepositoryMetadata } from
|
|||
*/
|
||||
export class GitFetcher {
|
||||
private readonly cacheDir: string
|
||||
private readonly metadataScanner: MetadataScanner
|
||||
private metadataScanner: MetadataScanner
|
||||
private git?: SimpleGit
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this.cacheDir = path.join(context.globalStorageUri.fsPath, "package-manager-cache")
|
||||
this.metadataScanner = new MetadataScanner()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize git instance for a repository
|
||||
* @param repoDir Repository directory
|
||||
*/
|
||||
private initGit(repoDir: string): void {
|
||||
this.git = simpleGit(repoDir)
|
||||
// Update MetadataScanner with new git instance
|
||||
this.metadataScanner = new MetadataScanner(this.git)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch repository data
|
||||
* @param repoUrl Repository URL
|
||||
|
|
@ -41,6 +52,9 @@ export class GitFetcher {
|
|||
// Clone or pull repository
|
||||
await this.cloneOrPullRepository(repoUrl, repoDir, forceRefresh)
|
||||
|
||||
// Initialize git for this repository
|
||||
this.initGit(repoDir)
|
||||
|
||||
// Validate repository structure
|
||||
await this.validateRepositoryStructure(repoDir)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as path from "path"
|
|||
import * as fs from "fs/promises"
|
||||
import * as vscode from "vscode"
|
||||
import * as yaml from "js-yaml"
|
||||
import { SimpleGit } from "simple-git"
|
||||
import { validateAnyMetadata } from "./schemas"
|
||||
import { ComponentMetadata, ComponentType, LocalizedMetadata, PackageManagerItem, PackageMetadata } from "./types"
|
||||
|
||||
|
|
@ -9,6 +10,12 @@ import { ComponentMetadata, ComponentType, LocalizedMetadata, PackageManagerItem
|
|||
* Handles component discovery and metadata loading
|
||||
*/
|
||||
export class MetadataScanner {
|
||||
private readonly git?: SimpleGit
|
||||
|
||||
constructor(git?: SimpleGit) {
|
||||
this.git = git
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans a directory for components
|
||||
* @param rootDir The root directory to scan
|
||||
|
|
@ -138,11 +145,31 @@ export class MetadataScanner {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the last modified date for a component
|
||||
* Gets the last modified date for a component using git history
|
||||
* @param componentDir The component directory
|
||||
* @returns ISO date string
|
||||
*/
|
||||
private async getLastModifiedDate(componentDir: string): Promise<string> {
|
||||
if (this.git) {
|
||||
try {
|
||||
// Get the latest commit date for the directory and its contents
|
||||
const result = await this.git.raw([
|
||||
"log",
|
||||
"-1",
|
||||
"--format=%aI", // ISO 8601 format
|
||||
"--",
|
||||
componentDir,
|
||||
])
|
||||
if (result) {
|
||||
return result.trim()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error getting git history for ${componentDir}:`, error)
|
||||
// Fall through to fs.stat fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to fs.stat if git is not available or fails
|
||||
try {
|
||||
const stats = await fs.stat(componentDir)
|
||||
return stats.mtime.toISOString()
|
||||
|
|
|
|||
161
src/services/package-manager/__tests__/GitDateTracking.test.ts
Normal file
161
src/services/package-manager/__tests__/GitDateTracking.test.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import * as fs from "fs/promises"
|
||||
import { Dirent, Stats } from "fs"
|
||||
import { SimpleGit } from "simple-git"
|
||||
import { MetadataScanner } from "../MetadataScanner"
|
||||
|
||||
// Mock fs/promises
|
||||
jest.mock("fs/promises")
|
||||
|
||||
// Mock simple-git
|
||||
jest.mock("simple-git", () => {
|
||||
const mockGit = {
|
||||
raw: jest.fn(),
|
||||
}
|
||||
return jest.fn(() => mockGit)
|
||||
})
|
||||
|
||||
// Create mock Dirent objects
|
||||
const createMockDirent = (name: string, isDir: boolean): Dirent => {
|
||||
return {
|
||||
name,
|
||||
isDirectory: () => isDir,
|
||||
isFile: () => !isDir,
|
||||
isBlockDevice: () => false,
|
||||
isCharacterDevice: () => false,
|
||||
isFIFO: () => false,
|
||||
isSocket: () => false,
|
||||
isSymbolicLink: () => false,
|
||||
// These are readonly in the real Dirent
|
||||
path: "",
|
||||
parentPath: "",
|
||||
} as Dirent
|
||||
}
|
||||
|
||||
describe("Git Date Tracking", () => {
|
||||
let metadataScanner: MetadataScanner
|
||||
let mockGit: jest.Mocked<SimpleGit>
|
||||
const mockFs = fs as jest.Mocked<typeof fs>
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Setup git mock
|
||||
mockGit = {
|
||||
raw: jest.fn(),
|
||||
} as unknown as jest.Mocked<SimpleGit>
|
||||
|
||||
metadataScanner = new MetadataScanner(mockGit)
|
||||
})
|
||||
|
||||
it("should use git log date when available", async () => {
|
||||
const mockDate = "2025-04-12T22:08:02-07:00"
|
||||
mockGit.raw.mockResolvedValue(mockDate)
|
||||
|
||||
// Mock directory structure
|
||||
mockFs.readdir.mockImplementation((path: any, options?: any) => {
|
||||
return Promise.resolve([createMockDirent("component1", true)])
|
||||
})
|
||||
|
||||
mockFs.readFile.mockImplementation((path: any) => {
|
||||
return Promise.resolve(`
|
||||
name: Test Component
|
||||
description: A test component
|
||||
type: mcp server
|
||||
version: 1.0.0
|
||||
`)
|
||||
})
|
||||
|
||||
const items = await metadataScanner.scanDirectory("/test/repo", "https://example.com")
|
||||
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].lastUpdated).toBe(mockDate)
|
||||
expect(mockGit.raw).toHaveBeenCalledWith([
|
||||
"log",
|
||||
"-1",
|
||||
"--format=%aI",
|
||||
"--",
|
||||
expect.stringContaining("component1"),
|
||||
])
|
||||
})
|
||||
|
||||
it("should fall back to fs.stat when git log fails", async () => {
|
||||
const mockDate = new Date()
|
||||
mockGit.raw.mockRejectedValue(new Error("Git error"))
|
||||
|
||||
// Mock fs.stat to return a specific date
|
||||
const mockStats = {
|
||||
mtime: mockDate,
|
||||
isFile: () => false,
|
||||
isDirectory: () => true,
|
||||
dev: 0,
|
||||
ino: 0,
|
||||
mode: 0,
|
||||
nlink: 0,
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
rdev: 0,
|
||||
size: 0,
|
||||
blksize: 0,
|
||||
blocks: 0,
|
||||
atimeMs: 0,
|
||||
mtimeMs: 0,
|
||||
ctimeMs: 0,
|
||||
birthtimeMs: 0,
|
||||
atime: new Date(),
|
||||
ctime: new Date(),
|
||||
birthtime: new Date(),
|
||||
} as Stats
|
||||
|
||||
mockFs.stat.mockResolvedValue(mockStats)
|
||||
|
||||
// Mock directory structure
|
||||
mockFs.readdir.mockImplementation((path: any, options?: any) => {
|
||||
return Promise.resolve([createMockDirent("component1", true)])
|
||||
})
|
||||
|
||||
mockFs.readFile.mockImplementation((path: any) => {
|
||||
return Promise.resolve(`
|
||||
name: Test Component
|
||||
description: A test component
|
||||
type: mcp server
|
||||
version: 1.0.0
|
||||
`)
|
||||
})
|
||||
|
||||
const items = await metadataScanner.scanDirectory("/test/repo", "https://example.com")
|
||||
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].lastUpdated).toBe(mockDate.toISOString())
|
||||
expect(mockFs.stat).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should fall back to current date when both git and fs.stat fail", async () => {
|
||||
const beforeTest = new Date()
|
||||
mockGit.raw.mockRejectedValue(new Error("Git error"))
|
||||
mockFs.stat.mockRejectedValue(new Error("Stat error"))
|
||||
|
||||
// Mock directory structure
|
||||
mockFs.readdir.mockImplementation((path: any, options?: any) => {
|
||||
return Promise.resolve([createMockDirent("component1", true)])
|
||||
})
|
||||
|
||||
mockFs.readFile.mockImplementation((path: any) => {
|
||||
return Promise.resolve(`
|
||||
name: Test Component
|
||||
description: A test component
|
||||
type: mcp server
|
||||
version: 1.0.0
|
||||
`)
|
||||
})
|
||||
|
||||
const items = await metadataScanner.scanDirectory("/test/repo", "https://example.com")
|
||||
const afterTest = new Date()
|
||||
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].lastUpdated).toBeDefined()
|
||||
const lastUpdated = new Date(items[0].lastUpdated!)
|
||||
expect(lastUpdated.getTime()).toBeGreaterThanOrEqual(beforeTest.getTime())
|
||||
expect(lastUpdated.getTime()).toBeLessThanOrEqual(afterTest.getTime())
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue