commit-analyzer 1.0.2 → 1.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.
Files changed (62) hide show
  1. package/.claude/settings.local.json +11 -1
  2. package/README.md +33 -2
  3. package/commits.csv +2 -0
  4. package/eslint.config.mts +45 -0
  5. package/package.json +17 -9
  6. package/src/1.domain/analysis.ts +93 -0
  7. package/src/1.domain/analyzed-commit.ts +97 -0
  8. package/src/1.domain/application-error.ts +32 -0
  9. package/src/1.domain/category.ts +52 -0
  10. package/src/1.domain/commit-analysis-service.ts +92 -0
  11. package/src/1.domain/commit-hash.ts +40 -0
  12. package/src/1.domain/commit.ts +99 -0
  13. package/src/1.domain/date-formatting-service.ts +81 -0
  14. package/src/1.domain/date-range.ts +76 -0
  15. package/src/1.domain/report-generation-service.ts +292 -0
  16. package/src/2.application/analyze-commits.usecase.ts +307 -0
  17. package/src/2.application/generate-report.usecase.ts +204 -0
  18. package/src/2.application/llm-service.ts +54 -0
  19. package/src/2.application/resume-analysis.usecase.ts +123 -0
  20. package/src/3.presentation/analysis-repository.interface.ts +27 -0
  21. package/src/3.presentation/analyze-command.ts +128 -0
  22. package/src/3.presentation/cli-application.ts +255 -0
  23. package/src/3.presentation/command-handler.interface.ts +4 -0
  24. package/src/3.presentation/commit-analysis-controller.ts +101 -0
  25. package/src/3.presentation/commit-repository.interface.ts +47 -0
  26. package/src/3.presentation/console-formatter.ts +129 -0
  27. package/src/3.presentation/progress-repository.interface.ts +49 -0
  28. package/src/3.presentation/report-command.ts +50 -0
  29. package/src/3.presentation/resume-command.ts +59 -0
  30. package/src/3.presentation/storage-repository.interface.ts +33 -0
  31. package/src/3.presentation/storage-service.interface.ts +32 -0
  32. package/src/3.presentation/version-control-service.interface.ts +41 -0
  33. package/src/4.infrastructure/cache-service.ts +271 -0
  34. package/src/4.infrastructure/cached-analysis-repository.ts +46 -0
  35. package/src/4.infrastructure/claude-llm-adapter.ts +124 -0
  36. package/src/4.infrastructure/csv-service.ts +206 -0
  37. package/src/4.infrastructure/file-storage-repository.ts +108 -0
  38. package/src/4.infrastructure/file-system-storage-adapter.ts +87 -0
  39. package/src/4.infrastructure/gemini-llm-adapter.ts +46 -0
  40. package/src/4.infrastructure/git-adapter.ts +116 -0
  41. package/src/4.infrastructure/git-commit-repository.ts +85 -0
  42. package/src/4.infrastructure/json-progress-tracker.ts +182 -0
  43. package/src/4.infrastructure/llm-adapter-factory.ts +26 -0
  44. package/src/4.infrastructure/llm-adapter.ts +455 -0
  45. package/src/4.infrastructure/llm-analysis-repository.ts +38 -0
  46. package/src/4.infrastructure/openai-llm-adapter.ts +57 -0
  47. package/src/di.ts +108 -0
  48. package/src/main.ts +63 -0
  49. package/src/utils/app-paths.ts +36 -0
  50. package/src/utils/concurrency.ts +81 -0
  51. package/src/utils.ts +77 -0
  52. package/tsconfig.json +7 -1
  53. package/src/cli.ts +0 -170
  54. package/src/csv-reader.ts +0 -180
  55. package/src/csv.ts +0 -40
  56. package/src/errors.ts +0 -49
  57. package/src/git.ts +0 -112
  58. package/src/index.ts +0 -395
  59. package/src/llm.ts +0 -396
  60. package/src/progress.ts +0 -84
  61. package/src/report-generator.ts +0 -286
  62. package/src/types.ts +0 -24
@@ -0,0 +1,206 @@
1
+ import { Analysis } from "@domain/analysis"
2
+ import { AnalyzedCommit } from "@domain/analyzed-commit"
3
+ import { Category } from "@domain/category"
4
+ import { Commit } from "@domain/commit"
5
+ import { CommitHash } from "@domain/commit-hash"
6
+
7
+ import { IStorageService } from "@presentation/storage-service.interface"
8
+
9
+ export class CSVService {
10
+ private static readonly CSV_HEADERS = "timestamp,category,summary,description"
11
+ private static readonly CSV_SPECIAL_CHARS = [",", '"', "\n"]
12
+
13
+ constructor(private readonly storageService: IStorageService) {}
14
+
15
+ async exportToCSV(
16
+ commits: AnalyzedCommit[],
17
+ filePath: string,
18
+ ): Promise<void> {
19
+ const csvContent = this.generateCSV(commits)
20
+ await this.storageService.writeFile(filePath, csvContent)
21
+ }
22
+
23
+ async importFromCSV(filePath: string): Promise<AnalyzedCommit[]> {
24
+ const content = await this.storageService.readFile(filePath)
25
+ return this.parseCSV(content)
26
+ }
27
+
28
+ private generateCSV(commits: AnalyzedCommit[]): string {
29
+ const rows = commits.map((commit) => this.formatRow(commit))
30
+ return [CSVService.CSV_HEADERS, ...rows].join("\n")
31
+ }
32
+
33
+ private formatRow(commit: AnalyzedCommit): string {
34
+ const row = commit.toCSVRow()
35
+ return this.joinCsvFields(row)
36
+ }
37
+
38
+ private joinCsvFields(row: {
39
+ timestamp: string
40
+ category: string
41
+ summary: string
42
+ description: string
43
+ }): string {
44
+ return [
45
+ row.timestamp,
46
+ this.escapeCsvField(row.category),
47
+ this.escapeCsvField(row.summary),
48
+ this.escapeCsvField(row.description),
49
+ ].join(",")
50
+ }
51
+
52
+ private escapeCsvField(field: string): string {
53
+ if (this.needsEscaping(field)) {
54
+ return this.escapeAndQuoteField(field)
55
+ }
56
+ return field
57
+ }
58
+
59
+ private needsEscaping(field: string): boolean {
60
+ return CSVService.CSV_SPECIAL_CHARS.some((char) => field.includes(char))
61
+ }
62
+
63
+ private escapeAndQuoteField(field: string): string {
64
+ return `"${field.replace(/"/g, '""')}"`
65
+ }
66
+
67
+ private parseCSV(content: string): AnalyzedCommit[] {
68
+ const lines = content.split("\n").filter((line) => line.trim().length > 0)
69
+
70
+ if (lines.length < 2) {
71
+ throw new Error("Invalid CSV format: no data rows found")
72
+ }
73
+
74
+ // Validate header
75
+ const header = lines[0].toLowerCase()
76
+ const expectedHeader = "timestamp,category,summary,description"
77
+ if (header !== expectedHeader) {
78
+ throw new Error(
79
+ `Invalid CSV format. Expected header: "${expectedHeader}", got: "${header}"`,
80
+ )
81
+ }
82
+
83
+ // Skip header row
84
+ const dataRows = lines.slice(1)
85
+ const commits: AnalyzedCommit[] = []
86
+
87
+ for (let i = 0; i < dataRows.length; i++) {
88
+ const row = dataRows[i]
89
+ try {
90
+ const commit = this.parseCSVRow(row)
91
+ commits.push(commit)
92
+ } catch (error) {
93
+ console.warn(`Warning: Failed to parse CSV row ${i + 2}: ${row}`)
94
+ console.warn(`Error: ${error}`)
95
+ }
96
+ }
97
+
98
+ return commits
99
+ }
100
+
101
+ private parseCSVRow(row: string): AnalyzedCommit {
102
+ const fields = this.parseCSVFields(row)
103
+
104
+ if (fields.length !== 4) {
105
+ throw new Error(
106
+ `Expected 4 fields (timestamp,category,summary,description), got ${fields.length}`,
107
+ )
108
+ }
109
+
110
+ const [timestampStr, category, summary, description] = fields
111
+
112
+ // Validate timestamp
113
+ const timestamp = new Date(timestampStr)
114
+ if (isNaN(timestamp.getTime())) {
115
+ throw new Error(`Invalid timestamp: ${timestampStr}`)
116
+ }
117
+
118
+ // Validate category
119
+ if (!this.isValidCategory(category)) {
120
+ throw new Error(
121
+ `Invalid category: ${category}. Must be one of: tweak, feature, process`,
122
+ )
123
+ }
124
+
125
+ // Validate required fields
126
+ if (!summary.trim()) {
127
+ throw new Error("Summary field cannot be empty")
128
+ }
129
+
130
+ if (!description.trim()) {
131
+ throw new Error("Description field cannot be empty")
132
+ }
133
+
134
+ // Create a minimal commit object for CSV import
135
+ // We'll use placeholder values for hash and diff since they're not in the CSV
136
+ const placeholderHash = CommitHash.create(
137
+ "0000000000000000000000000000000000000000",
138
+ )
139
+ const placeholderDiff = "# Placeholder diff for CSV import\n+1\n-0" // Minimal valid diff
140
+ const placeholderMessage = summary // Use summary as message
141
+
142
+ const commit = new Commit({
143
+ hash: placeholderHash,
144
+ message: placeholderMessage,
145
+ date: timestamp, // Use actual timestamp from CSV
146
+ diff: placeholderDiff,
147
+ })
148
+
149
+ // Create analysis from CSV data
150
+ const analysisCategory = Category.create(category)
151
+ const analysis = new Analysis({
152
+ category: analysisCategory,
153
+ summary: summary.trim(),
154
+ description: description.trim(),
155
+ })
156
+
157
+ return new AnalyzedCommit(commit, analysis)
158
+ }
159
+
160
+ /**
161
+ * Parse CSV fields handling quoted fields with commas and escaped quotes
162
+ */
163
+ private parseCSVFields(line: string): string[] {
164
+ const fields: string[] = []
165
+ let currentField = ""
166
+ let inQuotes = false
167
+ let i = 0
168
+
169
+ while (i < line.length) {
170
+ const char = line[i]
171
+ const nextChar = line[i + 1]
172
+
173
+ if (char === '"') {
174
+ if (inQuotes && nextChar === '"') {
175
+ // Escaped quote inside quoted field
176
+ currentField += '"'
177
+ i += 2
178
+ } else {
179
+ // Start or end of quoted field
180
+ inQuotes = !inQuotes
181
+ i++
182
+ }
183
+ } else if (char === "," && !inQuotes) {
184
+ // Field separator outside quotes
185
+ fields.push(currentField)
186
+ currentField = ""
187
+ i++
188
+ } else {
189
+ // Regular character
190
+ currentField += char
191
+ i++
192
+ }
193
+ }
194
+
195
+ // Add the last field
196
+ fields.push(currentField)
197
+
198
+ return fields
199
+ }
200
+
201
+ private isValidCategory(
202
+ category: string,
203
+ ): category is "tweak" | "feature" | "process" {
204
+ return ["tweak", "feature", "process"].includes(category)
205
+ }
206
+ }
@@ -0,0 +1,108 @@
1
+ import { AnalyzedCommit } from "@domain/analyzed-commit"
2
+
3
+ import { IStorageRepository } from "@presentation/storage-repository.interface"
4
+ import { IStorageService } from "@presentation/storage-service.interface"
5
+
6
+ import { CSVService } from "./csv-service"
7
+
8
+ export class FileStorageRepository implements IStorageRepository {
9
+ private readonly csvService: CSVService
10
+
11
+ constructor(private readonly storageService: IStorageService) {
12
+ this.csvService = new CSVService(storageService)
13
+ }
14
+
15
+ async exportToCSV(
16
+ commits: AnalyzedCommit[],
17
+ filePath: string,
18
+ ): Promise<void> {
19
+ await this.csvService.exportToCSV(commits, filePath)
20
+ }
21
+
22
+ async importFromCSV(filePath: string): Promise<AnalyzedCommit[]> {
23
+ return this.csvService.importFromCSV(filePath)
24
+ }
25
+
26
+ async generateReport(
27
+ commits: AnalyzedCommit[],
28
+ outputPath: string,
29
+ ): Promise<void> {
30
+ // Generate markdown report content
31
+ const reportContent = this.generateMarkdownReport(commits)
32
+ await this.storageService.writeFile(outputPath, reportContent)
33
+ }
34
+
35
+ async readCommitHashesFromFile(filePath: string): Promise<string[]> {
36
+ return this.storageService.readLines(filePath)
37
+ }
38
+
39
+ async ensureDirectoryExists(directoryPath: string): Promise<void> {
40
+ await this.storageService.ensureDirectory(directoryPath)
41
+ }
42
+
43
+ async writeFile(filePath: string, content: string): Promise<void> {
44
+ await this.storageService.writeFile(filePath, content)
45
+ }
46
+
47
+ private generateMarkdownReport(commits: AnalyzedCommit[]): string {
48
+ let content = "# Development Summary Report\n\n"
49
+
50
+ // Basic statistics
51
+ const totalCommits = commits.length
52
+ const years = commits.map((c) => c.getYear())
53
+ const minYear = Math.min(...years)
54
+ const maxYear = Math.max(...years)
55
+
56
+ const categoryBreakdown = commits.reduce(
57
+ (acc, commit) => {
58
+ const category = commit.getAnalysis().getCategory().getValue()
59
+ acc[category] = (acc[category] || 0) + 1
60
+ return acc
61
+ },
62
+ {} as Record<string, number>,
63
+ )
64
+
65
+ content += `## Analysis Summary\n\n`
66
+ content += `**Total Commits Analyzed:** ${totalCommits}\n`
67
+ content += `**Time Period:** ${minYear} - ${maxYear}\n\n`
68
+
69
+ content += `### Breakdown by Category\n\n`
70
+ content += `- **Features:** ${categoryBreakdown.feature || 0} commits\n`
71
+ content += `- **Process/Infrastructure:** ${categoryBreakdown.process || 0} commits\n`
72
+ content += `- **Tweaks/Fixes:** ${categoryBreakdown.tweak || 0} commits\n\n`
73
+
74
+ // Group by year and add yearly summaries
75
+ const commitsByYear = new Map<number, AnalyzedCommit[]>()
76
+ for (const commit of commits) {
77
+ const year = commit.getYear()
78
+ if (!commitsByYear.has(year)) {
79
+ commitsByYear.set(year, [])
80
+ }
81
+ commitsByYear.get(year)!.push(commit)
82
+ }
83
+
84
+ content += `## Yearly Development Highlights\n\n`
85
+
86
+ const sortedYears = Array.from(commitsByYear.keys()).sort((a, b) => b - a)
87
+ for (const year of sortedYears) {
88
+ const yearCommits = commitsByYear.get(year)!
89
+ const features = yearCommits.filter((c) =>
90
+ c.getAnalysis().isFeatureAnalysis(),
91
+ )
92
+
93
+ content += `### ${year}\n\n`
94
+ content += `${yearCommits.length} commits total, including ${features.length} new features.\n\n`
95
+
96
+ // Show top features
97
+ if (features.length > 0) {
98
+ content += `**Key Features:**\n`
99
+ for (const feature of features.slice(0, 5)) {
100
+ content += `- ${feature.getAnalysis().getSummary()}\n`
101
+ }
102
+ content += "\n"
103
+ }
104
+ }
105
+
106
+ return content
107
+ }
108
+ }
@@ -0,0 +1,87 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ unlinkSync,
6
+ writeFileSync,
7
+ } from "fs"
8
+ import { dirname, join } from "path"
9
+
10
+ import { IStorageService } from "@presentation/storage-service.interface"
11
+
12
+ import { getErrorMessage } from "../utils"
13
+
14
+ export class FileSystemStorageAdapter implements IStorageService {
15
+ private static readonly DEFAULT_ENCODING = "utf8"
16
+
17
+ async writeFile(filePath: string, content: string): Promise<void> {
18
+ try {
19
+ // Ensure directory exists
20
+ const dir = dirname(filePath)
21
+ if (!existsSync(dir)) {
22
+ mkdirSync(dir, { recursive: true })
23
+ }
24
+
25
+ writeFileSync(
26
+ filePath,
27
+ content,
28
+ FileSystemStorageAdapter.DEFAULT_ENCODING,
29
+ )
30
+ } catch (error) {
31
+ throw new Error(
32
+ `Failed to write file ${filePath}: ${getErrorMessage(error)}`,
33
+ )
34
+ }
35
+ }
36
+
37
+ async readFile(filePath: string): Promise<string> {
38
+ try {
39
+ return readFileSync(filePath, FileSystemStorageAdapter.DEFAULT_ENCODING)
40
+ } catch (error) {
41
+ throw new Error(
42
+ `Failed to read file ${filePath}: ${getErrorMessage(error)}`,
43
+ )
44
+ }
45
+ }
46
+
47
+ async fileExists(filePath: string): Promise<boolean> {
48
+ return existsSync(filePath)
49
+ }
50
+
51
+ async ensureDirectory(directoryPath: string): Promise<void> {
52
+ try {
53
+ mkdirSync(directoryPath, { recursive: true })
54
+ } catch (error) {
55
+ throw new Error(
56
+ `Failed to create directory ${directoryPath}: ${getErrorMessage(error)}`,
57
+ )
58
+ }
59
+ }
60
+
61
+ async deleteFile(filePath: string): Promise<void> {
62
+ try {
63
+ if (await this.fileExists(filePath)) {
64
+ unlinkSync(filePath)
65
+ }
66
+ } catch (error) {
67
+ throw new Error(
68
+ `Failed to delete file ${filePath}: ${getErrorMessage(error)}`,
69
+ )
70
+ }
71
+ }
72
+
73
+ async readLines(filePath: string): Promise<string[]> {
74
+ const content = await this.readFile(filePath)
75
+ return content
76
+ .split("\n")
77
+ .map((line) => line.trim())
78
+ .filter((line) => line.length > 0)
79
+ }
80
+
81
+ resolveOutputPath(filename: string, outputDir?: string): string {
82
+ if (outputDir) {
83
+ return join(outputDir, filename)
84
+ }
85
+ return filename
86
+ }
87
+ }
@@ -0,0 +1,46 @@
1
+ import { execSync } from "child_process"
2
+
3
+ import { LLMAdapter } from "./llm-adapter"
4
+
5
+ export class GeminiLLMAdapter extends LLMAdapter {
6
+ private static readonly MAX_PROMPT_LENGTH = parseInt(
7
+ process.env.GEMINI_MAX_PROMPT_LENGTH || "100000",
8
+ 10,
9
+ )
10
+
11
+ protected getMaxPromptLength(): number {
12
+ return GeminiLLMAdapter.MAX_PROMPT_LENGTH
13
+ }
14
+
15
+ async detectAvailableModels(): Promise<string[]> {
16
+ try {
17
+ execSync("command -v gemini", { stdio: "ignore" })
18
+ return ["gemini"]
19
+ } catch {
20
+ return []
21
+ }
22
+ }
23
+
24
+ async isAvailable(): Promise<boolean> {
25
+ const available = await this.detectAvailableModels()
26
+ return available.length > 0
27
+ }
28
+
29
+ protected async executeModelCommand(prompt: string): Promise<string> {
30
+ const truncatedPrompt = this.truncatePrompt(prompt)
31
+
32
+ if (this.verbose) {
33
+ console.log(` - Prompt length: ${truncatedPrompt.length} characters`)
34
+ }
35
+
36
+ const modelCommand = this.model || "gemini"
37
+
38
+ return execSync(modelCommand, {
39
+ input: truncatedPrompt,
40
+ encoding: "utf8",
41
+ stdio: ["pipe", "pipe", "pipe"],
42
+ timeout: LLMAdapter.DEFAULT_TIMEOUT,
43
+ })
44
+ }
45
+ }
46
+
@@ -0,0 +1,116 @@
1
+ import { execSync } from "child_process"
2
+
3
+ import { IVersionControlService } from "@presentation/version-control-service.interface"
4
+
5
+ import { getErrorMessage } from "../utils"
6
+
7
+ export class GitAdapter implements IVersionControlService {
8
+ private static readonly LARGE_DIFF_BUFFER = 50 * 1024 * 1024 // 50MB buffer for large diffs
9
+ private static readonly EXEC_OPTIONS = {
10
+ encoding: "utf8" as const,
11
+ stdio: ["pipe", "pipe", "pipe"] as ["pipe", "pipe", "pipe"],
12
+ }
13
+
14
+ async getCommitInfo(hash: string): Promise<{
15
+ hash: string
16
+ message: string
17
+ date: Date
18
+ diff: string
19
+ }> {
20
+ try {
21
+ const showOutput = execSync(
22
+ `git show --format="%H|%s|%ci" --no-patch "${hash}"`,
23
+ GitAdapter.EXEC_OPTIONS,
24
+ ).trim()
25
+
26
+ const [fullHash, message, dateStr] = showOutput.split("|")
27
+ const date = new Date(dateStr)
28
+
29
+ const diff = execSync(`git show "${hash}"`, {
30
+ ...GitAdapter.EXEC_OPTIONS,
31
+ maxBuffer: GitAdapter.LARGE_DIFF_BUFFER,
32
+ })
33
+
34
+ return {
35
+ hash: fullHash,
36
+ message,
37
+ date,
38
+ diff,
39
+ }
40
+ } catch (error) {
41
+ throw new Error(
42
+ `Failed to get commit info for ${hash}: ${getErrorMessage(error)}`,
43
+ )
44
+ }
45
+ }
46
+
47
+ async validateCommitHash(hash: string): Promise<boolean> {
48
+ try {
49
+ execSync(`git rev-parse --verify "${hash}"`, GitAdapter.EXEC_OPTIONS)
50
+ return true
51
+ } catch {
52
+ return false
53
+ }
54
+ }
55
+
56
+ async isValidRepository(): Promise<boolean> {
57
+ try {
58
+ execSync("git rev-parse --git-dir", GitAdapter.EXEC_OPTIONS)
59
+ return true
60
+ } catch {
61
+ return false
62
+ }
63
+ }
64
+
65
+ async getCurrentUserEmail(): Promise<string> {
66
+ try {
67
+ return execSync("git config user.email", GitAdapter.EXEC_OPTIONS).trim()
68
+ } catch (error) {
69
+ throw new Error(
70
+ `Failed to get current user email: ${getErrorMessage(error)}`,
71
+ )
72
+ }
73
+ }
74
+
75
+ async getCurrentUserName(): Promise<string> {
76
+ try {
77
+ return execSync("git config user.name", GitAdapter.EXEC_OPTIONS).trim()
78
+ } catch (error) {
79
+ throw new Error(
80
+ `Failed to get current user name: ${getErrorMessage(error)}`,
81
+ )
82
+ }
83
+ }
84
+
85
+ async getUserAuthoredCommits(params: {
86
+ authorEmail: string
87
+ limit?: number
88
+ since?: string
89
+ until?: string
90
+ }): Promise<string[]> {
91
+ const { authorEmail, limit, since, until } = params
92
+ try {
93
+ const limitFlag = limit ? `--max-count=${limit}` : ""
94
+ const sinceFlag = since ? `--since="${since}"` : ""
95
+ const untilFlag = until ? `--until="${until}"` : ""
96
+
97
+ const output = execSync(
98
+ `git log --author="${authorEmail}" --format="%H" --no-merges ${limitFlag} ${sinceFlag} ${untilFlag}`,
99
+ GitAdapter.EXEC_OPTIONS,
100
+ ).trim()
101
+
102
+ return this.parseCommitHashes(output)
103
+ } catch (error) {
104
+ throw new Error(
105
+ `Failed to get user authored commits: ${getErrorMessage(error)}`,
106
+ )
107
+ }
108
+ }
109
+
110
+ private parseCommitHashes(output: string): string[] {
111
+ if (!output) {
112
+ return []
113
+ }
114
+ return output.split("\n").filter((hash) => hash.length > 0)
115
+ }
116
+ }
@@ -0,0 +1,85 @@
1
+ import { Commit } from "@domain/commit"
2
+ import { CommitHash } from "@domain/commit-hash"
3
+
4
+ import { ICommitRepository } from "@presentation/commit-repository.interface"
5
+ import { IVersionControlService } from "@presentation/version-control-service.interface"
6
+
7
+ export class GitCommitRepository implements ICommitRepository {
8
+ constructor(private readonly versionControlService: IVersionControlService) {}
9
+
10
+ async getByHash(hash: CommitHash): Promise<Commit> {
11
+ const commitInfo = await this.versionControlService.getCommitInfo(
12
+ hash.getValue(),
13
+ )
14
+
15
+ return new Commit({
16
+ hash,
17
+ message: commitInfo.message,
18
+ date: commitInfo.date,
19
+ diff: commitInfo.diff,
20
+ })
21
+ }
22
+
23
+ async getByAuthor(params: {
24
+ authorEmail: string
25
+ limit?: number
26
+ since?: string
27
+ until?: string
28
+ }): Promise<Commit[]> {
29
+ const { authorEmail, limit, since, until } = params
30
+ const commitHashes =
31
+ await this.versionControlService.getUserAuthoredCommits({
32
+ authorEmail,
33
+ limit,
34
+ since,
35
+ until,
36
+ })
37
+ const commits: Commit[] = []
38
+
39
+ for (const hashString of commitHashes) {
40
+ try {
41
+ const hash = CommitHash.create(hashString)
42
+ const commit = await this.getByHash(hash)
43
+ commits.push(commit)
44
+ } catch (error) {
45
+ console.warn(`Warning: Failed to load commit ${hashString}:`, error)
46
+ }
47
+ }
48
+
49
+ return commits
50
+ }
51
+
52
+ async getByHashes(hashes: CommitHash[]): Promise<Commit[]> {
53
+ const commits: Commit[] = []
54
+
55
+ for (const hash of hashes) {
56
+ try {
57
+ const commit = await this.getByHash(hash)
58
+ commits.push(commit)
59
+ } catch (error) {
60
+ console.warn(
61
+ `Warning: Failed to load commit ${hash.getShortHash()}:`,
62
+ error,
63
+ )
64
+ }
65
+ }
66
+
67
+ return commits
68
+ }
69
+
70
+ async exists(hash: CommitHash): Promise<boolean> {
71
+ return this.versionControlService.validateCommitHash(hash.getValue())
72
+ }
73
+
74
+ async getCurrentUserEmail(): Promise<string> {
75
+ return this.versionControlService.getCurrentUserEmail()
76
+ }
77
+
78
+ async getCurrentUserName(): Promise<string> {
79
+ return this.versionControlService.getCurrentUserName()
80
+ }
81
+
82
+ async isValidRepository(): Promise<boolean> {
83
+ return this.versionControlService.isValidRepository()
84
+ }
85
+ }