commit-analyzer 1.0.3 → 1.1.1

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 +18 -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 +209 -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 +46 -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 +252 -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 +143 -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 +109 -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 -411
  60. package/src/progress.ts +0 -84
  61. package/src/report-generator.ts +0 -286
  62. package/src/types.ts +0 -24
package/src/main.ts ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { ApplicationError } from "@domain/application-error"
4
+
5
+ import { ConsoleFormatter } from "@presentation/console-formatter"
6
+
7
+ import { DIContainer } from "./di"
8
+
9
+ async function main(): Promise<void> {
10
+ try {
11
+ // Extract options from command line args before creating container
12
+ const llmOption = extractLLMOption(process.argv)
13
+ const noCacheOption = extractNoCacheOption(process.argv)
14
+ const container = new DIContainer({
15
+ llm: llmOption,
16
+ noCache: noCacheOption
17
+ })
18
+ const app = container.getApplication()
19
+
20
+ await app.run(process.argv)
21
+ } catch (error) {
22
+ if (error instanceof ApplicationError) {
23
+ ConsoleFormatter.logError(`[${error.code}]: ${error.message}`)
24
+ process.exit(1)
25
+ }
26
+
27
+ if (error instanceof Error) {
28
+ ConsoleFormatter.logError(`Unexpected error: ${error.message}`)
29
+ process.exit(1)
30
+ }
31
+
32
+ ConsoleFormatter.logError("Unknown error occurred")
33
+ process.exit(1)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Extract the --llm option from command line arguments
39
+ * This is needed before creating the DI container
40
+ */
41
+ function extractLLMOption(args: string[]): string | undefined {
42
+ const llmIndex = args.findIndex(arg => arg === '--llm')
43
+ if (llmIndex !== -1 && llmIndex + 1 < args.length) {
44
+ return args[llmIndex + 1]
45
+ }
46
+ return undefined
47
+ }
48
+
49
+ /**
50
+ * Extract the --no-cache option from command line arguments
51
+ * This is needed before creating the DI container
52
+ */
53
+ function extractNoCacheOption(args: string[]): boolean {
54
+ return args.includes('--no-cache')
55
+ }
56
+
57
+ // Run the application if this file is executed directly
58
+ if (require.main === module) {
59
+ main().catch((error) => {
60
+ ConsoleFormatter.logError(`Failed to bootstrap application: ${error}`)
61
+ process.exit(1)
62
+ })
63
+ }
@@ -0,0 +1,36 @@
1
+ import path from "path"
2
+
3
+ /**
4
+ * Utility for managing application data directory paths
5
+ */
6
+ export class AppPaths {
7
+ private static readonly APP_DATA_DIR = ".commit-analyzer"
8
+
9
+ /**
10
+ * Get the application data directory path
11
+ */
12
+ static getAppDataDir(baseDir: string = process.cwd()): string {
13
+ return path.join(baseDir, AppPaths.APP_DATA_DIR)
14
+ }
15
+
16
+ /**
17
+ * Get the cache directory path
18
+ */
19
+ static getCacheDir(baseDir: string = process.cwd()): string {
20
+ return path.join(AppPaths.getAppDataDir(baseDir), "cache")
21
+ }
22
+
23
+ /**
24
+ * Get the progress file path
25
+ */
26
+ static getProgressFilePath(baseDir: string = process.cwd()): string {
27
+ return path.join(AppPaths.getAppDataDir(baseDir), "progress.json")
28
+ }
29
+
30
+ /**
31
+ * Get any file path within the app data directory
32
+ */
33
+ static getAppDataFilePath(fileName: string, baseDir: string = process.cwd()): string {
34
+ return path.join(AppPaths.getAppDataDir(baseDir), fileName)
35
+ }
36
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Utility for managing concurrent operations with semaphore-like behavior
3
+ */
4
+ export class ConcurrencyManager {
5
+ private running = 0
6
+ private queue: (() => void)[] = []
7
+
8
+ constructor(private readonly maxConcurrency: number) {}
9
+
10
+ /**
11
+ * Execute a function with concurrency control
12
+ */
13
+ async execute<T>(fn: () => Promise<T>): Promise<T> {
14
+ return new Promise((resolve, reject) => {
15
+ const runTask = async () => {
16
+ this.running++
17
+ try {
18
+ const result = await fn()
19
+ resolve(result)
20
+ } catch (error) {
21
+ reject(error)
22
+ } finally {
23
+ this.running--
24
+ this.processQueue()
25
+ }
26
+ }
27
+
28
+ if (this.running < this.maxConcurrency) {
29
+ runTask()
30
+ } else {
31
+ this.queue.push(runTask)
32
+ }
33
+ })
34
+ }
35
+
36
+ private processQueue(): void {
37
+ if (this.queue.length > 0 && this.running < this.maxConcurrency) {
38
+ const nextTask = this.queue.shift()!
39
+ nextTask()
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Process items in parallel with controlled concurrency
46
+ */
47
+ export async function processInParallel<T, R>(
48
+ items: T[],
49
+ processor: (item: T, index: number) => Promise<R>,
50
+ maxConcurrency: number = 5
51
+ ): Promise<R[]> {
52
+ const manager = new ConcurrencyManager(maxConcurrency)
53
+ const promises = items.map((item, index) =>
54
+ manager.execute(() => processor(item, index))
55
+ )
56
+ return Promise.all(promises)
57
+ }
58
+
59
+ /**
60
+ * Process items in batches with parallel processing within each batch
61
+ */
62
+ export async function processInBatches<T, R>(
63
+ items: T[],
64
+ processor: (item: T, index: number) => Promise<R>,
65
+ batchSize: number = 10,
66
+ maxConcurrencyPerBatch: number = 5
67
+ ): Promise<R[]> {
68
+ const results: R[] = []
69
+
70
+ for (let i = 0; i < items.length; i += batchSize) {
71
+ const batch = items.slice(i, i + batchSize)
72
+ const batchResults = await processInParallel(
73
+ batch,
74
+ (item, batchIndex) => processor(item, i + batchIndex),
75
+ maxConcurrencyPerBatch
76
+ )
77
+ results.push(...batchResults)
78
+ }
79
+
80
+ return results
81
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,77 @@
1
+ import readline from "readline"
2
+
3
+ /**
4
+ * Common utility functions used across the application
5
+ */
6
+
7
+ /**
8
+ * Extracts error message from unknown error type
9
+ */
10
+ export function getErrorMessage(error: unknown): string {
11
+ return error instanceof Error ? error.message : "Unknown error"
12
+ }
13
+
14
+ /**
15
+ * Creates a promise-based readline interface for user input
16
+ */
17
+ export function createPromiseReadline(): {
18
+ question: (prompt: string) => Promise<string>
19
+ close: () => void
20
+ } {
21
+ const rl = readline.createInterface({
22
+ input: process.stdin,
23
+ output: process.stdout,
24
+ })
25
+
26
+ return {
27
+ question: (prompt: string): Promise<string> => {
28
+ return new Promise((resolve) => {
29
+ rl.question(prompt, (answer: string) => {
30
+ resolve(answer.trim())
31
+ })
32
+ })
33
+ },
34
+ close: () => rl.close(),
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Sleep utility for async delays
40
+ */
41
+ export function sleep(ms: number): Promise<void> {
42
+ return new Promise((resolve) => setTimeout(resolve, ms))
43
+ }
44
+
45
+ /**
46
+ * Formats file size in human readable format
47
+ */
48
+ export function formatFileSize(bytes: number): string {
49
+ const units = ["B", "KB", "MB", "GB"]
50
+ let size = bytes
51
+ let unitIndex = 0
52
+
53
+ while (size >= 1024 && unitIndex < units.length - 1) {
54
+ size /= 1024
55
+ unitIndex++
56
+ }
57
+
58
+ return `${size.toFixed(1)} ${units[unitIndex]}`
59
+ }
60
+
61
+ /**
62
+ * Truncates text to specified length with ellipsis
63
+ */
64
+ export function truncateText(text: string, maxLength: number): string {
65
+ if (text.length <= maxLength) {
66
+ return text
67
+ }
68
+ return text.substring(0, maxLength - 3) + "..."
69
+ }
70
+
71
+ /**
72
+ * Calculates percentage with proper rounding
73
+ */
74
+ export function calculatePercentage(part: number, total: number): number {
75
+ if (total === 0) return 0
76
+ return Math.round((part / total) * 100)
77
+ }
package/tsconfig.json CHANGED
@@ -12,7 +12,13 @@
12
12
  "resolveJsonModule": true,
13
13
  "declaration": true,
14
14
  "declarationMap": true,
15
- "sourceMap": true
15
+ "sourceMap": true,
16
+ "paths": {
17
+ "@domain/*": ["./src/1.domain/*"],
18
+ "@app/*": ["./src/2.application/*"],
19
+ "@presentation/*": ["./src/3.presentation/*"],
20
+ "@infra/*": ["./src/4.infrastructure/*"]
21
+ }
16
22
  },
17
23
  "include": ["src/**/*"],
18
24
  "exclude": ["node_modules", "dist"]
package/src/cli.ts DELETED
@@ -1,170 +0,0 @@
1
- import { readFileSync, mkdirSync } from "fs"
2
- import { Command } from "commander"
3
- import { join, dirname } from "path"
4
-
5
- export interface CLIOptions {
6
- output?: string
7
- outputDir?: string
8
- file?: string
9
- commits: string[]
10
- author?: string
11
- limit?: number
12
- useDefaults: boolean
13
- resume?: boolean
14
- clear?: boolean
15
- model?: string
16
- report?: boolean
17
- inputCsv?: string
18
- verbose?: boolean
19
- }
20
-
21
- export class CLIService {
22
- static parseArguments(): CLIOptions {
23
- const program = new Command()
24
-
25
- program
26
- .name("commit-analyzer")
27
- .description(
28
- "Analyze user authored git commits and generate rich commit descriptions and stakeholder reports from them.",
29
- )
30
- .version("1.0.3")
31
- .option("-o, --output <file>", "Output CSV file (default: commits.csv)")
32
- .option(
33
- "--output-dir <dir>",
34
- "Output directory for CSV and report files (default: current directory)",
35
- )
36
- .option(
37
- "-f, --file <file>",
38
- "Read commit hashes from file (one per line)",
39
- )
40
- .option(
41
- "-a, --author <email>",
42
- "Filter commits by author email (defaults to current user)",
43
- )
44
- .option(
45
- "-l, --limit <number>",
46
- "Limit number of commits to analyze",
47
- parseInt,
48
- )
49
- .option("-r, --resume", "Resume from last checkpoint if available")
50
- .option("-c, --clear", "Clear any existing progress checkpoint")
51
- .option("-m, --model <model>", "LLM model to use (claude, gemini, codex)")
52
- .option(
53
- "--report",
54
- "Generate condensed markdown report from existing CSV",
55
- )
56
- .option(
57
- "--input-csv <file>",
58
- "Input CSV file to read for report generation",
59
- )
60
- .option(
61
- "-v, --verbose",
62
- "Enable verbose logging (shows detailed error information)",
63
- )
64
- .argument(
65
- "[commits...]",
66
- "Commit hashes to analyze (if none provided, uses current user's commits)",
67
- )
68
- .parse()
69
-
70
- const options = program.opts()
71
- const args = program.args
72
-
73
- let commits: string[] = []
74
- let useDefaults = false
75
-
76
- if (options.file) {
77
- commits = this.readCommitsFromFile(options.file)
78
- } else if (args.length > 0) {
79
- commits = args
80
- } else {
81
- useDefaults = true
82
- }
83
-
84
- return {
85
- output:
86
- options.output ||
87
- CLIService.resolveOutputPath("commits.csv", options.outputDir),
88
- outputDir: options.outputDir,
89
- file: options.file,
90
- commits,
91
- author: options.author,
92
- limit: options.limit,
93
- useDefaults,
94
- resume: options.resume,
95
- clear: options.clear,
96
- model: options.model,
97
- report: options.report,
98
- inputCsv: options.inputCsv,
99
- verbose: options.verbose,
100
- }
101
- }
102
-
103
- private static readCommitsFromFile(filename: string): string[] {
104
- try {
105
- const content = readFileSync(filename, "utf8")
106
- return content
107
- .split("\n")
108
- .map((line) => line.trim())
109
- .filter((line) => line.length > 0)
110
- } catch (error) {
111
- throw new Error(
112
- `Failed to read commits from file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}`,
113
- )
114
- }
115
- }
116
-
117
- /**
118
- * Resolve the full file path with optional output directory.
119
- */
120
- static resolveOutputPath(filename: string, outputDir?: string): string {
121
- if (outputDir) {
122
- // Ensure output directory exists
123
- try {
124
- mkdirSync(outputDir, { recursive: true })
125
- } catch (error) {
126
- throw new Error(
127
- `Failed to create output directory ${outputDir}: ${error instanceof Error ? error.message : "Unknown error"}`,
128
- )
129
- }
130
- return join(outputDir, filename)
131
- }
132
- return filename
133
- }
134
-
135
- static showHelp(): void {
136
- console.log(`
137
- Usage: commit-analyzer [options] [commits...]
138
-
139
- Analyze git commits and generate categorized summaries using LLM.
140
- If no commits are specified, analyzes all commits authored by the current user.
141
-
142
- Options:
143
- -o, --output <file> Output file (default: commits.csv for analysis, report.md for reports)
144
- --output-dir <dir> Output directory for CSV and report files (default: current directory)
145
- -f, --file <file> Read commit hashes from file (one per line)
146
- -a, --author <email> Filter commits by author email (defaults to current user)
147
- -l, --limit <number> Limit number of commits to analyze
148
- -r, --resume Resume from last checkpoint if available
149
- -c, --clear Clear any existing progress checkpoint
150
- --report Generate condensed markdown report from existing CSV
151
- --input-csv <file> Input CSV file to read for report generation
152
- -v, --verbose Enable verbose logging (shows detailed error information)
153
- -h, --help Display help for command
154
- -V, --version Display version number
155
-
156
- Examples:
157
- commit-analyzer # Analyze your authored commits
158
- commit-analyzer --limit 10 # Analyze your last 10 commits
159
- commit-analyzer --author user@example.com # Analyze specific user's commits
160
- commit-analyzer abc123 def456 ghi789 # Analyze specific commits
161
- commit-analyzer --file commits.txt # Read commits from file
162
- commit-analyzer --output analysis.csv --limit 20 # Analyze last 20 commits to custom file
163
- commit-analyzer --resume # Resume from last checkpoint
164
- commit-analyzer --clear # Clear checkpoint and start fresh
165
- commit-analyzer --report # Analyze commits, generate CSV, then generate report
166
- commit-analyzer --input-csv data.csv --report # Skip analysis, generate report from existing CSV
167
- commit-analyzer --report -o custom-report.md # Analyze commits, generate CSV, then generate custom report
168
- `)
169
- }
170
- }
package/src/csv-reader.ts DELETED
@@ -1,180 +0,0 @@
1
- import { readFileSync } from "fs"
2
-
3
- export interface ParsedCSVRow {
4
- year: number
5
- category: "tweak" | "feature" | "process"
6
- summary: string
7
- description: string
8
- }
9
-
10
- export class CSVReaderService {
11
- static readCSV(filename: string): ParsedCSVRow[] {
12
- try {
13
- const content = readFileSync(filename, "utf8")
14
- return this.parseCSV(content)
15
- } catch (error) {
16
- throw new Error(
17
- `Failed to read CSV file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}`,
18
- )
19
- }
20
- }
21
-
22
- private static parseCSV(content: string): ParsedCSVRow[] {
23
- const lines = content.trim().split("\n")
24
-
25
- if (lines.length === 0) {
26
- throw new Error("CSV file is empty")
27
- }
28
-
29
- // Validate header
30
- const header = lines[0].toLowerCase()
31
- const expectedHeader = "year,category,summary,description"
32
- if (header !== expectedHeader) {
33
- throw new Error(
34
- `Invalid CSV format. Expected header: "${expectedHeader}", got: "${header}"`,
35
- )
36
- }
37
-
38
- const rows: ParsedCSVRow[] = []
39
-
40
- for (let i = 1; i < lines.length; i++) {
41
- const line = lines[i].trim()
42
- if (line === "") continue // Skip empty lines
43
-
44
- try {
45
- const row = this.parseCSVLine(line, i + 1)
46
- rows.push(row)
47
- } catch (error) {
48
- throw new Error(
49
- `Error parsing CSV line ${i + 1}: ${error instanceof Error ? error.message : "Unknown error"}`,
50
- )
51
- }
52
- }
53
-
54
- return rows
55
- }
56
-
57
- private static parseCSVLine(line: string, lineNumber: number): ParsedCSVRow {
58
- const fields = this.parseCSVFields(line)
59
-
60
- if (fields.length !== 4) {
61
- throw new Error(
62
- `Expected 4 fields (year,category,summary,description), got ${fields.length}`,
63
- )
64
- }
65
-
66
- const [yearStr, category, summary, description] = fields
67
-
68
- // Validate year
69
- const year = parseInt(yearStr, 10)
70
- if (isNaN(year) || year < 1900 || year > 2100) {
71
- throw new Error(`Invalid year: ${yearStr}`)
72
- }
73
-
74
- // Validate category
75
- if (!this.isValidCategory(category)) {
76
- throw new Error(
77
- `Invalid category: ${category}. Must be one of: tweak, feature, process`,
78
- )
79
- }
80
-
81
- // Validate required fields
82
- if (!summary.trim()) {
83
- throw new Error("Summary field cannot be empty")
84
- }
85
-
86
- if (!description.trim()) {
87
- throw new Error("Description field cannot be empty")
88
- }
89
-
90
- return {
91
- year,
92
- category: category as "tweak" | "feature" | "process",
93
- summary: summary.trim(),
94
- description: description.trim(),
95
- }
96
- }
97
-
98
- /**
99
- * Parse CSV fields handling quoted fields with commas and escaped quotes
100
- */
101
- private static parseCSVFields(line: string): string[] {
102
- const fields: string[] = []
103
- let currentField = ""
104
- let inQuotes = false
105
- let i = 0
106
-
107
- while (i < line.length) {
108
- const char = line[i]
109
- const nextChar = line[i + 1]
110
-
111
- if (char === '"') {
112
- if (inQuotes && nextChar === '"') {
113
- // Escaped quote inside quoted field
114
- currentField += '"'
115
- i += 2
116
- } else {
117
- // Start or end of quoted field
118
- inQuotes = !inQuotes
119
- i++
120
- }
121
- } else if (char === "," && !inQuotes) {
122
- // Field separator outside quotes
123
- fields.push(currentField)
124
- currentField = ""
125
- i++
126
- } else {
127
- // Regular character
128
- currentField += char
129
- i++
130
- }
131
- }
132
-
133
- // Add the last field
134
- fields.push(currentField)
135
-
136
- return fields
137
- }
138
-
139
- private static isValidCategory(
140
- category: string,
141
- ): category is "tweak" | "feature" | "process" {
142
- return ["tweak", "feature", "process"].includes(category)
143
- }
144
-
145
- /**
146
- * Get summary statistics about the CSV data
147
- */
148
- static getStatistics(rows: ParsedCSVRow[]): {
149
- totalRows: number
150
- yearRange: { min: number; max: number }
151
- categoryBreakdown: Record<string, number>
152
- } {
153
- if (rows.length === 0) {
154
- return {
155
- totalRows: 0,
156
- yearRange: { min: 0, max: 0 },
157
- categoryBreakdown: { tweak: 0, feature: 0, process: 0 },
158
- }
159
- }
160
-
161
- const years = rows.map((row) => row.year)
162
- const categoryBreakdown = rows.reduce(
163
- (acc, row) => {
164
- acc[row.category]++
165
- return acc
166
- },
167
- { tweak: 0, feature: 0, process: 0 } as Record<string, number>,
168
- )
169
-
170
- return {
171
- totalRows: rows.length,
172
- yearRange: {
173
- min: Math.min(...years),
174
- max: Math.max(...years),
175
- },
176
- categoryBreakdown,
177
- }
178
- }
179
- }
180
-
package/src/csv.ts DELETED
@@ -1,40 +0,0 @@
1
- import { writeFileSync } from "fs"
2
- import { AnalyzedCommit, CSVRow } from "./types"
3
-
4
- export class CSVService {
5
- static generateCSV(commits: AnalyzedCommit[]): string {
6
- const headers = "year,category,summary,description"
7
- const rows = commits.map((commit) => this.formatRow(commit))
8
-
9
- return [headers, ...rows].join("\n")
10
- }
11
-
12
- static exportToFile(commits: AnalyzedCommit[], filename: string): void {
13
- const csvContent = this.generateCSV(commits)
14
- writeFileSync(filename, csvContent, "utf8")
15
- }
16
-
17
- private static formatRow(commit: AnalyzedCommit): string {
18
- const row: CSVRow = {
19
- year: commit.year,
20
- category: commit.analysis.category,
21
- summary: commit.analysis.summary,
22
- description: commit.analysis.description,
23
- }
24
-
25
- return [
26
- row.year,
27
- this.escapeCsvField(row.category),
28
- this.escapeCsvField(row.summary),
29
- this.escapeCsvField(row.description),
30
- ].join(",")
31
- }
32
-
33
- private static escapeCsvField(field: string): string {
34
- if (field.includes(",") || field.includes('"') || field.includes("\n")) {
35
- return `"${field.replace(/"/g, '""')}"`
36
- }
37
- return field
38
- }
39
- }
40
-