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
@@ -4,7 +4,17 @@
4
4
  "Bash(mkdir:*)",
5
5
  "Bash(bun install:*)",
6
6
  "Bash(bun run:*)",
7
- "Bash(bun link:*)"
7
+ "Bash(bun link:*)",
8
+ "Bash(npm run typecheck:*)",
9
+ "Bash(npm run lint)",
10
+ "Bash(npm run build:*)",
11
+ "Bash(npm run dev:*)",
12
+ "Bash(bun dev:*)",
13
+ "Bash(git log:*)",
14
+ "Bash(git checkout:*)",
15
+ "Bash(npx ts-node:*)",
16
+ "Bash(node:*)",
17
+ "Bash(npm start:*)"
8
18
  ],
9
19
  "deny": [],
10
20
  "ask": []
package/README.md CHANGED
@@ -13,13 +13,26 @@ A TypeScript/Node.js program that analyzes git commits and generates categorized
13
13
  - Automatically filters out merge commits for cleaner analysis
14
14
  - Robust error handling and validation
15
15
 
16
+ ## Prerequisites
17
+
18
+ This tool requires Bun runtime. Install it globally:
19
+
20
+ ```bash
21
+ # Install bun globally
22
+ curl -fsSL https://bun.sh/install | bash
23
+ # or
24
+ npm install -g bun
25
+ ```
26
+
16
27
  ## Installation
17
28
 
18
29
  ```bash
19
30
  npm install
20
- npm run build
31
+ bun link
21
32
  ```
22
33
 
34
+ After linking, you can use `commit-analyzer` command globally.
35
+
23
36
  ## Usage
24
37
 
25
38
  ### Default Behavior
@@ -151,7 +164,7 @@ npx commit-analyzer --clear
151
164
  npx commit-analyzer --resume
152
165
  ```
153
166
 
154
- The checkpoint file (`.commit-analyzer-progress.json`) contains:
167
+ The checkpoint file (`.commit-analyzer/progress.json`) contains:
155
168
  - List of all commits to process
156
169
  - Successfully processed commits (including failed ones to skip on resume)
157
170
  - Analyzed commit data (only successful ones)
@@ -159,6 +172,24 @@ The checkpoint file (`.commit-analyzer-progress.json`) contains:
159
172
 
160
173
  **Important**: When a commit fails after all retries (default 3), the process stops immediately to prevent wasting API calls. The successfully analyzed commits up to that point are saved to the CSV file.
161
174
 
175
+ ### Application Data Directory
176
+
177
+ The tool creates a `.commit-analyzer/` directory to store internal files:
178
+
179
+ ```
180
+ .commit-analyzer/
181
+ ├── progress.json # Progress checkpoint data
182
+ └── cache/ # Cached analysis results
183
+ ├── commit-abc123.json
184
+ ├── commit-def456.json
185
+ └── ...
186
+ ```
187
+
188
+ - **Progress checkpoint**: Enables resuming interrupted analysis sessions
189
+ - **Analysis cache**: Stores LLM analysis results to avoid re-processing the same commits (TTL: 30 days)
190
+
191
+ Use `--no-cache` to disable caching if needed.
192
+
162
193
  ### Retry Logic
163
194
 
164
195
  The tool includes automatic retry logic with exponential backoff for handling API failures when processing many commits. This is especially useful when analyzing large numbers of commits that might trigger rate limits.
package/commits.csv ADDED
@@ -0,0 +1,2 @@
1
+ year,category,summary,description
2
+ 2025,process,"Refactor project structure with numbered layers, path aliases, and automated","Major architectural reorganization moving from nested directories to numbered layer structure (1.domain, 2.application, 3.presentation, 4.infrastructure). Implements TypeScript path aliases (@domain, @app, @presentation, @infra) for cleaner imports and adds ESLint plugin for automatic import sorting and organization. All files renamed to kebab-case and imports updated throughout the codebase to use the new alias system. This improves code maintainability, reduces import path complexity, and establishes consistent file organization patterns."
@@ -0,0 +1,45 @@
1
+ import js from "@eslint/js"
2
+ import { defineConfig } from "eslint/config"
3
+ import simpleImportSort from "eslint-plugin-simple-import-sort"
4
+ import globals from "globals"
5
+ import tseslint from "typescript-eslint"
6
+
7
+ export default defineConfig([
8
+ {
9
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
10
+ plugins: { js },
11
+ extends: ["js/recommended"],
12
+ languageOptions: { globals: globals.node },
13
+ },
14
+ tseslint.configs.recommended,
15
+ {
16
+ plugins: {
17
+ "simple-import-sort": simpleImportSort,
18
+ },
19
+ rules: {
20
+ "simple-import-sort/imports": [
21
+ "warn",
22
+ {
23
+ groups: [
24
+ // Side effect imports
25
+ ["^\\u0000"],
26
+ // Packages. `react` related packages come first.
27
+ ["^react", "^@?\\w"],
28
+ // Aliases (adjust these regexes for your aliases)
29
+ ["^@domain"],
30
+ ["^@app"],
31
+ ["^@presentation"],
32
+ ["^@infra"],
33
+ // Parent imports
34
+ ["^\\.\\.(?!/?$)", "^\\.\\./?$"],
35
+ // Relative imports
36
+ ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
37
+ // Style imports
38
+ ["^.+\\.s?css$"],
39
+ ],
40
+ },
41
+ ],
42
+ "simple-import-sort/exports": "warn",
43
+ },
44
+ },
45
+ ])
package/package.json CHANGED
@@ -1,20 +1,23 @@
1
1
  {
2
2
  "name": "commit-analyzer",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
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
- "main": "dist/index.js",
5
+ "main": "dist/main.ts",
6
6
  "bin": {
7
- "commit-analyzer": "dist/index.js"
7
+ "commit-analyzer": "dist/main.ts"
8
8
  },
9
9
  "prettier": {
10
10
  "semi": false
11
11
  },
12
12
  "scripts": {
13
- "build": "tsc",
14
- "start": "node dist/index.js",
15
- "dev": "ts-node src/index.ts",
13
+ "build": "bun build src/main.ts --outfile=dist/main.ts --compile",
14
+ "start": "bun src/main.ts",
15
+ "dev": "bun src/main.ts",
16
16
  "lint": "eslint src/**/*.ts",
17
- "typecheck": "tsc --noEmit"
17
+ "typecheck": "tsc --noEmit",
18
+ "link": "bun link",
19
+ "publish": "bun publish",
20
+ "deploy": "bun run build && bun link && bun publish"
18
21
  },
19
22
  "keywords": [
20
23
  "git",
@@ -26,12 +29,18 @@
26
29
  "author": "steverodri",
27
30
  "license": "MIT",
28
31
  "devDependencies": {
32
+ "@eslint/js": "^9.34.0",
29
33
  "@types/node": "^20.0.0",
30
34
  "@typescript-eslint/eslint-plugin": "^6.0.0",
31
35
  "@typescript-eslint/parser": "^6.0.0",
32
- "eslint": "^8.0.0",
36
+ "eslint": "^9.34.0",
37
+ "eslint-plugin-simple-import-sort": "^12.1.1",
38
+ "globals": "^16.3.0",
39
+ "jiti": "^2.5.1",
33
40
  "ts-node": "^10.0.0",
34
- "typescript": "^5.0.0"
41
+ "tsconfig-paths": "^4.2.0",
42
+ "typescript": "^5.0.0",
43
+ "typescript-eslint": "^8.40.0"
35
44
  },
36
45
  "dependencies": {
37
46
  "commander": "^11.0.0"
@@ -0,0 +1,93 @@
1
+ import { Category } from "./category"
2
+
3
+ /**
4
+ * Domain entity representing the analysis of a commit
5
+ */
6
+ export class Analysis {
7
+ private static readonly MAX_SUMMARY_LENGTH = 80
8
+ private static readonly MIN_DESCRIPTION_LENGTH = 10
9
+ private readonly category: Category
10
+ private readonly summary: string
11
+ private readonly description: string
12
+
13
+ constructor(params: {
14
+ category: Category
15
+ summary: string
16
+ description: string
17
+ }) {
18
+ const { category, summary, description } = params
19
+ this.category = category
20
+ this.summary = summary
21
+ this.description = description
22
+
23
+ if (!summary || summary.trim().length === 0) {
24
+ throw new Error("Summary cannot be empty")
25
+ }
26
+
27
+ if (!description || description.trim().length === 0) {
28
+ throw new Error("Description cannot be empty")
29
+ }
30
+
31
+ if (summary.length > Analysis.MAX_SUMMARY_LENGTH) {
32
+ throw new Error(
33
+ `Summary cannot exceed ${Analysis.MAX_SUMMARY_LENGTH} characters`,
34
+ )
35
+ }
36
+
37
+ if (description.length < Analysis.MIN_DESCRIPTION_LENGTH) {
38
+ throw new Error(
39
+ `Description must be at least ${Analysis.MIN_DESCRIPTION_LENGTH} characters`,
40
+ )
41
+ }
42
+ }
43
+
44
+ getCategory(): Category {
45
+ return this.category
46
+ }
47
+
48
+ getSummary(): string {
49
+ return this.summary
50
+ }
51
+
52
+ getDescription(): string {
53
+ return this.description
54
+ }
55
+
56
+ getSummaryTruncated(): string {
57
+ return this.summary.length > Analysis.MAX_SUMMARY_LENGTH
58
+ ? this.summary.substring(0, Analysis.MAX_SUMMARY_LENGTH - 3) + "..."
59
+ : this.summary
60
+ }
61
+
62
+ isFeatureAnalysis(): boolean {
63
+ return this.category.isFeature()
64
+ }
65
+
66
+ isTweakAnalysis(): boolean {
67
+ return this.category.isTweak()
68
+ }
69
+
70
+ isProcessAnalysis(): boolean {
71
+ return this.category.isProcess()
72
+ }
73
+
74
+ equals(other: Analysis): boolean {
75
+ return (
76
+ this.category.equals(other.category) &&
77
+ this.summary === other.summary &&
78
+ this.description === other.description
79
+ )
80
+ }
81
+
82
+ toPlainObject(): {
83
+ category: string
84
+ summary: string
85
+ description: string
86
+ } {
87
+ return {
88
+ category: this.category.getValue(),
89
+ summary: this.summary,
90
+ description: this.description,
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,97 @@
1
+ import { Analysis } from "./analysis"
2
+ import { Commit } from "./commit"
3
+ import { CommitHash } from "./commit-hash"
4
+
5
+ /**
6
+ * Domain entity representing a commit with its analysis
7
+ */
8
+ export class AnalyzedCommit {
9
+ constructor(
10
+ private readonly commit: Commit,
11
+ private readonly analysis: Analysis,
12
+ ) {
13
+ if (!commit) {
14
+ throw new Error("Commit is required")
15
+ }
16
+
17
+ if (!analysis) {
18
+ throw new Error("Analysis is required")
19
+ }
20
+ }
21
+
22
+ getCommit(): Commit {
23
+ return this.commit
24
+ }
25
+
26
+ getAnalysis(): Analysis {
27
+ return this.analysis
28
+ }
29
+
30
+ getHash(): CommitHash {
31
+ return this.commit.getHash()
32
+ }
33
+
34
+ getMessage(): string {
35
+ return this.commit.getMessage()
36
+ }
37
+
38
+ getDate(): Date {
39
+ return this.commit.getDate()
40
+ }
41
+
42
+ getYear(): number {
43
+ return this.commit.getYear()
44
+ }
45
+
46
+ getShortHash(length: number = 8): string {
47
+ return this.commit.getShortHash(length)
48
+ }
49
+
50
+ isLargeChange(): boolean {
51
+ return this.commit.isLargeChange()
52
+ }
53
+
54
+ equals(other: AnalyzedCommit): boolean {
55
+ return (
56
+ this.commit.equals(other.commit) && this.analysis.equals(other.analysis)
57
+ )
58
+ }
59
+
60
+ toCSVRow(): {
61
+ timestamp: string
62
+ category: string
63
+ summary: string
64
+ description: string
65
+ } {
66
+ return {
67
+ timestamp: this.getDate().toISOString(),
68
+ category: this.analysis.getCategory().getValue(),
69
+ summary: this.analysis.getSummary(),
70
+ description: this.analysis.getDescription(),
71
+ }
72
+ }
73
+
74
+ toReportData(): {
75
+ hash: string
76
+ shortHash: string
77
+ message: string
78
+ date: Date
79
+ year: number
80
+ category: string
81
+ summary: string
82
+ description: string
83
+ isLargeChange: boolean
84
+ } {
85
+ return {
86
+ hash: this.commit.getHash().getValue(),
87
+ shortHash: this.getShortHash(),
88
+ message: this.getMessage(),
89
+ date: this.getDate(),
90
+ year: this.getYear(),
91
+ category: this.analysis.getCategory().getValue(),
92
+ summary: this.analysis.getSummary(),
93
+ description: this.analysis.getDescription(),
94
+ isLargeChange: this.isLargeChange(),
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Base class for application errors
3
+ */
4
+ export abstract class ApplicationError extends Error {
5
+ constructor(
6
+ message: string,
7
+ public readonly code: string,
8
+ ) {
9
+ super(message)
10
+ this.name = "ApplicationError"
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Error for validation failures
16
+ */
17
+ export class ValidationError extends ApplicationError {
18
+ constructor(message: string) {
19
+ super(message, "VALIDATION_ERROR")
20
+ this.name = "ValidationError"
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Error for not found resources
26
+ */
27
+ export class NotFoundError extends ApplicationError {
28
+ constructor(resource: string, identifier: string) {
29
+ super(`${resource} not found: ${identifier}`, "NOT_FOUND")
30
+ this.name = "NotFoundError"
31
+ }
32
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Value object for commit analysis category
3
+ */
4
+ export type CategoryType = "tweak" | "feature" | "process"
5
+
6
+ export class Category {
7
+ private static readonly VALID_CATEGORIES: CategoryType[] = ["tweak", "feature", "process"]
8
+
9
+ private constructor(private readonly value: CategoryType) {}
10
+
11
+ static create(category: string): Category {
12
+ if (!category || typeof category !== 'string') {
13
+ throw new Error('Category cannot be empty')
14
+ }
15
+
16
+ const normalizedCategory = category.toLowerCase().trim() as CategoryType
17
+
18
+ if (!this.VALID_CATEGORIES.includes(normalizedCategory)) {
19
+ throw new Error(`Invalid category: ${category}. Must be one of: ${this.VALID_CATEGORIES.join(', ')}`)
20
+ }
21
+
22
+ return new Category(normalizedCategory)
23
+ }
24
+
25
+ static fromType(category: CategoryType): Category {
26
+ return new Category(category)
27
+ }
28
+
29
+ getValue(): CategoryType {
30
+ return this.value
31
+ }
32
+
33
+ equals(other: Category): boolean {
34
+ return this.value === other.value
35
+ }
36
+
37
+ toString(): string {
38
+ return this.value
39
+ }
40
+
41
+ isFeature(): boolean {
42
+ return this.value === "feature"
43
+ }
44
+
45
+ isTweak(): boolean {
46
+ return this.value === "tweak"
47
+ }
48
+
49
+ isProcess(): boolean {
50
+ return this.value === "process"
51
+ }
52
+ }
@@ -0,0 +1,92 @@
1
+ import { IAnalysisRepository } from "@presentation/analysis-repository.interface"
2
+ import { ICommitRepository } from "@presentation/commit-repository.interface"
3
+
4
+ import { AnalyzedCommit } from "./analyzed-commit"
5
+ import { Commit } from "./commit"
6
+ import { CommitHash } from "./commit-hash"
7
+
8
+ /**
9
+ * Domain service for commit analysis operations
10
+ */
11
+ export class CommitAnalysisService {
12
+ constructor(
13
+ private readonly commitRepository: ICommitRepository,
14
+ private readonly analysisRepository: IAnalysisRepository,
15
+ ) {}
16
+
17
+ /**
18
+ * Analyzes a single commit
19
+ */
20
+ async analyzeCommit(hash: CommitHash): Promise<AnalyzedCommit> {
21
+ const commit = await this.commitRepository.getByHash(hash)
22
+ const analysis = await this.analysisRepository.analyze(commit)
23
+
24
+ return new AnalyzedCommit(commit, analysis)
25
+ }
26
+
27
+ /**
28
+ * Analyzes multiple commits
29
+ */
30
+ async analyzeCommits(hashes: CommitHash[]): Promise<AnalyzedCommit[]> {
31
+ const analyzedCommits: AnalyzedCommit[] = []
32
+
33
+ for (const hash of hashes) {
34
+ try {
35
+ const analyzedCommit = await this.analyzeCommit(hash)
36
+ analyzedCommits.push(analyzedCommit)
37
+ } catch (error) {
38
+ // Re-throw the error - let the application layer handle logging
39
+ throw new Error(
40
+ `Failed to analyze commit ${hash.getShortHash()}: ${error instanceof Error ? error.message : String(error)}`,
41
+ )
42
+ }
43
+ }
44
+
45
+ return analyzedCommits
46
+ }
47
+
48
+ /**
49
+ * Validates that all commit hashes exist
50
+ */
51
+ async validateCommits(
52
+ hashes: CommitHash[],
53
+ ): Promise<{ valid: CommitHash[]; invalid: CommitHash[] }> {
54
+ const valid: CommitHash[] = []
55
+ const invalid: CommitHash[] = []
56
+
57
+ for (const hash of hashes) {
58
+ const exists = await this.commitRepository.exists(hash)
59
+ if (exists) {
60
+ valid.push(hash)
61
+ } else {
62
+ invalid.push(hash)
63
+ }
64
+ }
65
+
66
+ return { valid, invalid }
67
+ }
68
+
69
+ /**
70
+ * Gets commits authored by the current user
71
+ */
72
+ async getCurrentUserCommits(params?: {
73
+ limit?: number
74
+ since?: string
75
+ until?: string
76
+ }): Promise<Commit[]> {
77
+ const userEmail = await this.commitRepository.getCurrentUserEmail()
78
+ return this.commitRepository.getByAuthor({
79
+ authorEmail: userEmail,
80
+ limit: params?.limit,
81
+ since: params?.since,
82
+ until: params?.until,
83
+ })
84
+ }
85
+
86
+ /**
87
+ * Checks if the analysis service is ready
88
+ */
89
+ async isAnalysisServiceReady(): Promise<boolean> {
90
+ return this.analysisRepository.isAvailable()
91
+ }
92
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Value object for Git commit hash
3
+ */
4
+ export class CommitHash {
5
+ private constructor(private readonly value: string) {}
6
+
7
+ static create(hash: string): CommitHash {
8
+ if (!hash || typeof hash !== 'string') {
9
+ throw new Error('Commit hash cannot be empty')
10
+ }
11
+
12
+ // Git short hash is minimum 4 characters, full hash is 40
13
+ if (hash.length < 4 || hash.length > 40) {
14
+ throw new Error('Invalid commit hash length')
15
+ }
16
+
17
+ // Only hexadecimal characters allowed
18
+ if (!/^[a-f0-9]+$/i.test(hash)) {
19
+ throw new Error('Commit hash must contain only hexadecimal characters')
20
+ }
21
+
22
+ return new CommitHash(hash)
23
+ }
24
+
25
+ getValue(): string {
26
+ return this.value
27
+ }
28
+
29
+ getShortHash(length: number = 8): string {
30
+ return this.value.substring(0, Math.min(length, this.value.length))
31
+ }
32
+
33
+ equals(other: CommitHash): boolean {
34
+ return this.value === other.value
35
+ }
36
+
37
+ toString(): string {
38
+ return this.value
39
+ }
40
+ }
@@ -0,0 +1,99 @@
1
+ import { CommitHash } from "./commit-hash"
2
+
3
+ /**
4
+ * Domain entity representing a Git commit
5
+ */
6
+ export class Commit {
7
+ private readonly hash: CommitHash
8
+ private readonly message: string
9
+ private readonly date: Date
10
+ private readonly diff: string
11
+
12
+ constructor(params: {
13
+ hash: CommitHash
14
+ message: string
15
+ date: Date
16
+ diff: string
17
+ }) {
18
+ const { hash, message, date, diff } = params
19
+ this.hash = hash
20
+ this.message = message
21
+ this.date = date
22
+ this.diff = diff
23
+ if (!message || message.trim().length === 0) {
24
+ throw new Error("Commit message cannot be empty")
25
+ }
26
+
27
+ if (!date) {
28
+ throw new Error("Commit date is required")
29
+ }
30
+
31
+ if (!diff) {
32
+ throw new Error("Commit diff is required")
33
+ }
34
+ }
35
+
36
+ getHash(): CommitHash {
37
+ return this.hash
38
+ }
39
+
40
+ getMessage(): string {
41
+ return this.message
42
+ }
43
+
44
+ getDate(): Date {
45
+ return new Date(this.date)
46
+ }
47
+
48
+ getDiff(): string {
49
+ return this.diff
50
+ }
51
+
52
+ getYear(): number {
53
+ return this.date.getFullYear()
54
+ }
55
+
56
+ getShortHash(length: number = 8): string {
57
+ return this.hash.getShortHash(length)
58
+ }
59
+
60
+ getDiffStats(): {
61
+ additions: number
62
+ deletions: number
63
+ filesChanged: number
64
+ } {
65
+ const lines = this.diff.split("\n")
66
+ let additions = 0
67
+ let deletions = 0
68
+ const filesChanged = new Set<string>()
69
+
70
+ for (const line of lines) {
71
+ if (line.startsWith("+") && !line.startsWith("+++")) {
72
+ additions++
73
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
74
+ deletions++
75
+ } else if (line.startsWith("diff --git")) {
76
+ // Extract filename from diff header
77
+ const match = line.match(/diff --git a\/(.+) b\/(.+)/)
78
+ if (match) {
79
+ filesChanged.add(match[1])
80
+ }
81
+ }
82
+ }
83
+
84
+ return {
85
+ additions,
86
+ deletions,
87
+ filesChanged: filesChanged.size,
88
+ }
89
+ }
90
+
91
+ isLargeChange(): boolean {
92
+ const stats = this.getDiffStats()
93
+ return stats.additions + stats.deletions > 100 || stats.filesChanged > 10
94
+ }
95
+
96
+ equals(other: Commit): boolean {
97
+ return this.hash.equals(other.hash)
98
+ }
99
+ }