commit-analyzer 1.0.3 → 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.
- package/.claude/settings.local.json +11 -1
- package/README.md +33 -2
- package/commits.csv +2 -0
- package/eslint.config.mts +45 -0
- package/package.json +17 -9
- package/src/1.domain/analysis.ts +93 -0
- package/src/1.domain/analyzed-commit.ts +97 -0
- package/src/1.domain/application-error.ts +32 -0
- package/src/1.domain/category.ts +52 -0
- package/src/1.domain/commit-analysis-service.ts +92 -0
- package/src/1.domain/commit-hash.ts +40 -0
- package/src/1.domain/commit.ts +99 -0
- package/src/1.domain/date-formatting-service.ts +81 -0
- package/src/1.domain/date-range.ts +76 -0
- package/src/1.domain/report-generation-service.ts +292 -0
- package/src/2.application/analyze-commits.usecase.ts +307 -0
- package/src/2.application/generate-report.usecase.ts +204 -0
- package/src/2.application/llm-service.ts +54 -0
- package/src/2.application/resume-analysis.usecase.ts +123 -0
- package/src/3.presentation/analysis-repository.interface.ts +27 -0
- package/src/3.presentation/analyze-command.ts +128 -0
- package/src/3.presentation/cli-application.ts +255 -0
- package/src/3.presentation/command-handler.interface.ts +4 -0
- package/src/3.presentation/commit-analysis-controller.ts +101 -0
- package/src/3.presentation/commit-repository.interface.ts +47 -0
- package/src/3.presentation/console-formatter.ts +129 -0
- package/src/3.presentation/progress-repository.interface.ts +49 -0
- package/src/3.presentation/report-command.ts +50 -0
- package/src/3.presentation/resume-command.ts +59 -0
- package/src/3.presentation/storage-repository.interface.ts +33 -0
- package/src/3.presentation/storage-service.interface.ts +32 -0
- package/src/3.presentation/version-control-service.interface.ts +41 -0
- package/src/4.infrastructure/cache-service.ts +271 -0
- package/src/4.infrastructure/cached-analysis-repository.ts +46 -0
- package/src/4.infrastructure/claude-llm-adapter.ts +124 -0
- package/src/4.infrastructure/csv-service.ts +206 -0
- package/src/4.infrastructure/file-storage-repository.ts +108 -0
- package/src/4.infrastructure/file-system-storage-adapter.ts +87 -0
- package/src/4.infrastructure/gemini-llm-adapter.ts +46 -0
- package/src/4.infrastructure/git-adapter.ts +116 -0
- package/src/4.infrastructure/git-commit-repository.ts +85 -0
- package/src/4.infrastructure/json-progress-tracker.ts +182 -0
- package/src/4.infrastructure/llm-adapter-factory.ts +26 -0
- package/src/4.infrastructure/llm-adapter.ts +455 -0
- package/src/4.infrastructure/llm-analysis-repository.ts +38 -0
- package/src/4.infrastructure/openai-llm-adapter.ts +57 -0
- package/src/di.ts +108 -0
- package/src/main.ts +63 -0
- package/src/utils/app-paths.ts +36 -0
- package/src/utils/concurrency.ts +81 -0
- package/src/utils.ts +77 -0
- package/tsconfig.json +7 -1
- package/src/cli.ts +0 -170
- package/src/csv-reader.ts +0 -180
- package/src/csv.ts +0 -40
- package/src/errors.ts +0 -49
- package/src/git.ts +0 -112
- package/src/index.ts +0 -395
- package/src/llm.ts +0 -411
- package/src/progress.ts +0 -84
- package/src/report-generator.ts +0 -286
- 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
|
-
|
|
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
|
|
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,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commit-analyzer",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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/
|
|
5
|
+
"main": "dist/main.ts",
|
|
6
6
|
"bin": {
|
|
7
|
-
"commit-analyzer": "dist/
|
|
7
|
+
"commit-analyzer": "dist/main.ts"
|
|
8
8
|
},
|
|
9
9
|
"prettier": {
|
|
10
10
|
"semi": false
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "
|
|
14
|
-
"start": "
|
|
15
|
-
"dev": "
|
|
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"
|
|
18
20
|
},
|
|
19
21
|
"keywords": [
|
|
20
22
|
"git",
|
|
@@ -26,12 +28,18 @@
|
|
|
26
28
|
"author": "steverodri",
|
|
27
29
|
"license": "MIT",
|
|
28
30
|
"devDependencies": {
|
|
31
|
+
"@eslint/js": "^9.34.0",
|
|
29
32
|
"@types/node": "^20.0.0",
|
|
30
33
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
31
34
|
"@typescript-eslint/parser": "^6.0.0",
|
|
32
|
-
"eslint": "^
|
|
35
|
+
"eslint": "^9.34.0",
|
|
36
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
37
|
+
"globals": "^16.3.0",
|
|
38
|
+
"jiti": "^2.5.1",
|
|
33
39
|
"ts-node": "^10.0.0",
|
|
34
|
-
"
|
|
40
|
+
"tsconfig-paths": "^4.2.0",
|
|
41
|
+
"typescript": "^5.0.0",
|
|
42
|
+
"typescript-eslint": "^8.40.0"
|
|
35
43
|
},
|
|
36
44
|
"dependencies": {
|
|
37
45
|
"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
|
+
}
|