commit-analyzer 1.1.0 → 1.1.2
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.
- package/.claude/settings.local.json +2 -1
- package/package.json +3 -2
- package/src/2.application/generate-report.usecase.ts +6 -1
- package/src/3.presentation/cli-application.ts +26 -2
- package/src/3.presentation/version-control-service.interface.ts +5 -0
- package/src/4.infrastructure/csv-service.ts +50 -4
- package/src/4.infrastructure/git-adapter.ts +27 -0
- package/src/di.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commit-analyzer",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Analyze git commits and generate categories, summaries, and descriptions for each commit. Optionally generate a yearly breakdown report of your commit history.",
|
|
5
5
|
"main": "dist/main.ts",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"lint": "eslint src/**/*.ts",
|
|
17
17
|
"typecheck": "tsc --noEmit",
|
|
18
18
|
"link": "bun link",
|
|
19
|
-
"publish": "bun publish"
|
|
19
|
+
"publish": "bun publish",
|
|
20
|
+
"deploy": "bun run build && bun link && bun publish"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
22
23
|
"git",
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
import { ICommandHandler } from "@presentation/command-handler.interface"
|
|
9
9
|
import { IStorageRepository } from "@presentation/storage-repository.interface"
|
|
10
|
+
import { IVersionControlService } from "@presentation/version-control-service.interface"
|
|
10
11
|
|
|
11
12
|
import { ILLMService } from "./llm-service"
|
|
12
13
|
|
|
@@ -35,6 +36,7 @@ export class GenerateReportUseCase
|
|
|
35
36
|
private readonly storageRepository: IStorageRepository,
|
|
36
37
|
private readonly llmService: ILLMService,
|
|
37
38
|
private readonly dateFormattingService: DateFormattingService,
|
|
39
|
+
private readonly versionControlService: IVersionControlService,
|
|
38
40
|
) {}
|
|
39
41
|
|
|
40
42
|
async handle(command: GenerateReportCommand): Promise<GenerateReportResult> {
|
|
@@ -99,7 +101,10 @@ export class GenerateReportUseCase
|
|
|
99
101
|
sourceInfo?: { type: 'author' | 'commits' | 'csv'; value: string }
|
|
100
102
|
}): Promise<void> {
|
|
101
103
|
const { commits, statistics, outputPath, includeStatistics, sourceInfo } = params
|
|
102
|
-
|
|
104
|
+
|
|
105
|
+
// Get repository name for the report heading
|
|
106
|
+
const repositoryName = await this.versionControlService.getRepositoryName()
|
|
107
|
+
let reportContent = `# Development Report for ${repositoryName}\n\n`
|
|
103
108
|
|
|
104
109
|
if (includeStatistics) {
|
|
105
110
|
reportContent += this.generateAnalysisSection(commits, statistics, sourceInfo)
|
|
@@ -24,7 +24,7 @@ export interface CLIOptions {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export class CLIApplication {
|
|
27
|
-
private static readonly VERSION = "1.1.
|
|
27
|
+
private static readonly VERSION = "1.1.2"
|
|
28
28
|
private static readonly DEFAULT_COMMITS_OUTPUT_FILE = "results/commits.csv"
|
|
29
29
|
private static readonly DEFAULT_REPORT_OUTPUT_FILE = "results/report.md"
|
|
30
30
|
|
|
@@ -156,9 +156,11 @@ export class CLIApplication {
|
|
|
156
156
|
|
|
157
157
|
// Handle input CSV mode (report generation only)
|
|
158
158
|
if (options.inputCsv) {
|
|
159
|
+
const reportOutputPath = this.determineReportOutputPath(options)
|
|
160
|
+
|
|
159
161
|
await this.controller.handleReportGeneration({
|
|
160
162
|
inputCsv: options.inputCsv,
|
|
161
|
-
output:
|
|
163
|
+
output: reportOutputPath,
|
|
162
164
|
sourceInfo: { type: "csv", value: options.inputCsv },
|
|
163
165
|
})
|
|
164
166
|
return
|
|
@@ -252,4 +254,26 @@ export class CLIApplication {
|
|
|
252
254
|
}
|
|
253
255
|
return csvPath + ".md"
|
|
254
256
|
}
|
|
257
|
+
|
|
258
|
+
private determineReportOutputPath(options: CLIOptions): string {
|
|
259
|
+
// Check if explicit output or outputDir was provided in raw options
|
|
260
|
+
// We need to check raw options to distinguish between user-provided and default values
|
|
261
|
+
const hasExplicitOutput =
|
|
262
|
+
process.argv.includes("--output") || process.argv.includes("-o")
|
|
263
|
+
const hasExplicitOutputDir = process.argv.includes("--output-dir")
|
|
264
|
+
|
|
265
|
+
if (hasExplicitOutput || hasExplicitOutputDir) {
|
|
266
|
+
return options.output || CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Default: generate report in same directory as CSV with .md extension
|
|
270
|
+
return this.getReportPathFromCsv(options.inputCsv!)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private getReportPathFromCsv(csvPath: string): string {
|
|
274
|
+
if (csvPath.endsWith(".csv")) {
|
|
275
|
+
return csvPath.replace(/\.csv$/, ".md")
|
|
276
|
+
}
|
|
277
|
+
return csvPath + ".md"
|
|
278
|
+
}
|
|
255
279
|
}
|
|
@@ -64,15 +64,61 @@ export class CSVService {
|
|
|
64
64
|
return `"${field.replace(/"/g, '""')}"`
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Split CSV content into rows, properly handling quoted fields that may contain newlines
|
|
69
|
+
*/
|
|
70
|
+
private splitCSVIntoRows(content: string): string[] {
|
|
71
|
+
const rows: string[] = []
|
|
72
|
+
let currentRow = ""
|
|
73
|
+
let inQuotes = false
|
|
74
|
+
let i = 0
|
|
75
|
+
|
|
76
|
+
while (i < content.length) {
|
|
77
|
+
const char = content[i]
|
|
78
|
+
const nextChar = content[i + 1]
|
|
79
|
+
|
|
80
|
+
if (char === '"') {
|
|
81
|
+
if (inQuotes && nextChar === '"') {
|
|
82
|
+
// Escaped quote inside quoted field
|
|
83
|
+
currentRow += '""'
|
|
84
|
+
i += 2
|
|
85
|
+
} else {
|
|
86
|
+
// Start or end of quoted field
|
|
87
|
+
inQuotes = !inQuotes
|
|
88
|
+
currentRow += '"'
|
|
89
|
+
i++
|
|
90
|
+
}
|
|
91
|
+
} else if (char === "\n" && !inQuotes) {
|
|
92
|
+
// Row separator outside quotes
|
|
93
|
+
if (currentRow.trim().length > 0) {
|
|
94
|
+
rows.push(currentRow)
|
|
95
|
+
}
|
|
96
|
+
currentRow = ""
|
|
97
|
+
i++
|
|
98
|
+
} else {
|
|
99
|
+
// Regular character (including newlines inside quotes)
|
|
100
|
+
currentRow += char
|
|
101
|
+
i++
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add the last row if it exists
|
|
106
|
+
if (currentRow.trim().length > 0) {
|
|
107
|
+
rows.push(currentRow)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return rows
|
|
111
|
+
}
|
|
112
|
+
|
|
67
113
|
private parseCSV(content: string): AnalyzedCommit[] {
|
|
68
|
-
const
|
|
114
|
+
const rows = this.splitCSVIntoRows(content)
|
|
69
115
|
|
|
70
|
-
if (
|
|
116
|
+
if (rows.length < 2) {
|
|
71
117
|
throw new Error("Invalid CSV format: no data rows found")
|
|
72
118
|
}
|
|
73
119
|
|
|
74
120
|
// Validate header
|
|
75
|
-
const header =
|
|
121
|
+
const header = rows[0].toLowerCase()
|
|
76
122
|
const expectedHeader = "timestamp,category,summary,description"
|
|
77
123
|
if (header !== expectedHeader) {
|
|
78
124
|
throw new Error(
|
|
@@ -81,7 +127,7 @@ export class CSVService {
|
|
|
81
127
|
}
|
|
82
128
|
|
|
83
129
|
// Skip header row
|
|
84
|
-
const dataRows =
|
|
130
|
+
const dataRows = rows.slice(1)
|
|
85
131
|
const commits: AnalyzedCommit[] = []
|
|
86
132
|
|
|
87
133
|
for (let i = 0; i < dataRows.length; i++) {
|
|
@@ -82,6 +82,33 @@ export class GitAdapter implements IVersionControlService {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
async getRepositoryName(): Promise<string> {
|
|
86
|
+
try {
|
|
87
|
+
// Try to get the repository name from remote origin URL
|
|
88
|
+
const remoteUrl = execSync("git config --get remote.origin.url", GitAdapter.EXEC_OPTIONS).trim()
|
|
89
|
+
|
|
90
|
+
// Extract repository name from various URL formats
|
|
91
|
+
// git@github.com:user/repo.git -> repo
|
|
92
|
+
// https://github.com/user/repo.git -> repo
|
|
93
|
+
// https://github.com/user/repo -> repo
|
|
94
|
+
const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/)
|
|
95
|
+
if (match && match[1]) {
|
|
96
|
+
return match[1]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Fallback: get the directory name
|
|
100
|
+
const dirName = execSync("basename $(git rev-parse --show-toplevel)", GitAdapter.EXEC_OPTIONS).trim()
|
|
101
|
+
return dirName
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Final fallback: use current directory name
|
|
104
|
+
try {
|
|
105
|
+
return execSync("basename $(pwd)", GitAdapter.EXEC_OPTIONS).trim()
|
|
106
|
+
} catch {
|
|
107
|
+
return "Unknown Project"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
async getUserAuthoredCommits(params: {
|
|
86
113
|
authorEmail: string
|
|
87
114
|
limit?: number
|