commit-analyzer 1.1.4 → 1.1.6
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/README.md +164 -82
- package/dist/main.ts +0 -0
- package/package.json +2 -1
- package/.claude/settings.local.json +0 -23
- package/commits.csv +0 -2
- package/csv-to-report-prompt.md +0 -97
- package/eslint.config.mts +0 -45
- package/prompt.md +0 -69
- package/src/1.domain/analysis.ts +0 -93
- package/src/1.domain/analyzed-commit.ts +0 -97
- package/src/1.domain/application-error.ts +0 -32
- package/src/1.domain/category.ts +0 -52
- package/src/1.domain/commit-analysis-service.ts +0 -92
- package/src/1.domain/commit-hash.ts +0 -40
- package/src/1.domain/commit.ts +0 -99
- package/src/1.domain/date-formatting-service.ts +0 -81
- package/src/1.domain/date-range.ts +0 -76
- package/src/1.domain/report-generation-service.ts +0 -443
- package/src/2.application/analyze-commits.usecase.ts +0 -307
- package/src/2.application/generate-report.usecase.ts +0 -209
- package/src/2.application/llm-service.ts +0 -54
- package/src/2.application/resume-analysis.usecase.ts +0 -123
- package/src/3.presentation/analysis-repository.interface.ts +0 -27
- package/src/3.presentation/analyze-command.ts +0 -128
- package/src/3.presentation/cli-application.ts +0 -278
- package/src/3.presentation/command-handler.interface.ts +0 -4
- package/src/3.presentation/commit-analysis-controller.ts +0 -101
- package/src/3.presentation/commit-repository.interface.ts +0 -47
- package/src/3.presentation/console-formatter.ts +0 -129
- package/src/3.presentation/progress-repository.interface.ts +0 -49
- package/src/3.presentation/report-command.ts +0 -50
- package/src/3.presentation/resume-command.ts +0 -59
- package/src/3.presentation/storage-repository.interface.ts +0 -33
- package/src/3.presentation/storage-service.interface.ts +0 -32
- package/src/3.presentation/version-control-service.interface.ts +0 -46
- package/src/4.infrastructure/cache-service.ts +0 -271
- package/src/4.infrastructure/cached-analysis-repository.ts +0 -46
- package/src/4.infrastructure/claude-llm-adapter.ts +0 -124
- package/src/4.infrastructure/csv-service.ts +0 -252
- package/src/4.infrastructure/file-storage-repository.ts +0 -108
- package/src/4.infrastructure/file-system-storage-adapter.ts +0 -87
- package/src/4.infrastructure/gemini-llm-adapter.ts +0 -46
- package/src/4.infrastructure/git-adapter.ts +0 -143
- package/src/4.infrastructure/git-commit-repository.ts +0 -85
- package/src/4.infrastructure/json-progress-tracker.ts +0 -182
- package/src/4.infrastructure/llm-adapter-factory.ts +0 -26
- package/src/4.infrastructure/llm-adapter.ts +0 -485
- package/src/4.infrastructure/llm-analysis-repository.ts +0 -38
- package/src/4.infrastructure/openai-llm-adapter.ts +0 -57
- package/src/di.ts +0 -109
- package/src/main.ts +0 -63
- package/src/utils/app-paths.ts +0 -36
- package/src/utils/concurrency.ts +0 -81
- package/src/utils.ts +0 -77
- package/tsconfig.json +0 -25
package/src/1.domain/analysis.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
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
|
-
}
|
package/src/1.domain/category.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
}
|
package/src/1.domain/commit.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { AnalyzedCommit } from "./analyzed-commit"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Domain service for formatting date ranges based on time periods
|
|
5
|
-
*/
|
|
6
|
-
export class DateFormattingService {
|
|
7
|
-
/**
|
|
8
|
-
* Format date range based on the span of commits
|
|
9
|
-
*/
|
|
10
|
-
formatDateRange(commits: AnalyzedCommit[]): string {
|
|
11
|
-
const dates = commits.map(c => c.getDate())
|
|
12
|
-
const minDate = new Date(Math.min(...dates.map(d => d.getTime())))
|
|
13
|
-
const maxDate = new Date(Math.max(...dates.map(d => d.getTime())))
|
|
14
|
-
|
|
15
|
-
const diffInMilliseconds = maxDate.getTime() - minDate.getTime()
|
|
16
|
-
const diffInDays = diffInMilliseconds / (1000 * 60 * 60 * 24)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// For very small differences, use a more conservative approach
|
|
20
|
-
if (diffInDays < 0.05) { // Less than ~1 hour
|
|
21
|
-
return this.formatHourlyRange(minDate, maxDate)
|
|
22
|
-
} else if (diffInDays <= 1) {
|
|
23
|
-
return this.formatDailyRange(minDate, maxDate)
|
|
24
|
-
} else if (diffInDays <= 7) {
|
|
25
|
-
return this.formatWeeklyRange(minDate, maxDate)
|
|
26
|
-
} else if (diffInDays <= 31) {
|
|
27
|
-
return this.formatMonthlyRange(minDate, maxDate)
|
|
28
|
-
} else if (diffInDays <= 365) {
|
|
29
|
-
return this.formatMonthlyRange(minDate, maxDate)
|
|
30
|
-
} else {
|
|
31
|
-
return this.formatYearlyRange(minDate, maxDate)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
private formatHourlyRange(minDate: Date, maxDate: Date): string {
|
|
37
|
-
const formatTime = (date: Date) => {
|
|
38
|
-
const hour = date.getHours()
|
|
39
|
-
const ampm = hour < 12 ? 'am' : 'pm'
|
|
40
|
-
const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
|
|
41
|
-
return `${displayHour}${ampm}`
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// If same time, just show the single time
|
|
45
|
-
if (minDate.getTime() === maxDate.getTime()) {
|
|
46
|
-
return formatTime(minDate)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return `${formatTime(minDate)} to ${formatTime(maxDate)}`
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private formatDailyRange(minDate: Date, maxDate: Date): string {
|
|
53
|
-
const minDay = minDate.toLocaleDateString('en-US', { weekday: 'long' })
|
|
54
|
-
const maxDay = maxDate.toLocaleDateString('en-US', { weekday: 'long' })
|
|
55
|
-
return `${minDay} to ${maxDay}`
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private formatWeeklyRange(minDate: Date, maxDate: Date): string {
|
|
59
|
-
const formatWeekly = (date: Date) => `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`
|
|
60
|
-
return `${formatWeekly(minDate)} - ${formatWeekly(maxDate)}`
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private formatMonthlyRange(minDate: Date, maxDate: Date): string {
|
|
64
|
-
const minMonth = minDate.toLocaleDateString('en-US', { month: 'short' })
|
|
65
|
-
const maxMonth = maxDate.toLocaleDateString('en-US', { month: 'short' })
|
|
66
|
-
const minYear = minDate.getFullYear()
|
|
67
|
-
const maxYear = maxDate.getFullYear()
|
|
68
|
-
|
|
69
|
-
if (minYear === maxYear) {
|
|
70
|
-
return `${minMonth} - ${maxMonth}`
|
|
71
|
-
} else {
|
|
72
|
-
return `${minMonth} ${minYear} - ${maxMonth} ${maxYear}`
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private formatYearlyRange(minDate: Date, maxDate: Date): string {
|
|
77
|
-
const minYear = minDate.getFullYear()
|
|
78
|
-
const maxYear = maxDate.getFullYear()
|
|
79
|
-
return minYear === maxYear ? minYear.toString() : `${minYear} - ${maxYear}`
|
|
80
|
-
}
|
|
81
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Value object for date ranges in commit analysis
|
|
3
|
-
*/
|
|
4
|
-
export class DateRange {
|
|
5
|
-
private constructor(
|
|
6
|
-
private readonly startDate: Date,
|
|
7
|
-
private readonly endDate: Date,
|
|
8
|
-
) {}
|
|
9
|
-
|
|
10
|
-
static create(startDate: Date, endDate: Date): DateRange {
|
|
11
|
-
if (!startDate || !endDate) {
|
|
12
|
-
throw new Error('Start date and end date are required')
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (startDate > endDate) {
|
|
16
|
-
throw new Error('Start date cannot be after end date')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return new DateRange(new Date(startDate), new Date(endDate))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static fromYear(year: number): DateRange {
|
|
23
|
-
if (!Number.isInteger(year) || year < 1970) {
|
|
24
|
-
throw new Error('Invalid year provided')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const startDate = new Date(year, 0, 1) // January 1st
|
|
28
|
-
const endDate = new Date(year, 11, 31, 23, 59, 59, 999) // December 31st
|
|
29
|
-
|
|
30
|
-
return new DateRange(startDate, endDate)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
static fromYearRange(startYear: number, endYear: number): DateRange {
|
|
34
|
-
if (startYear > endYear) {
|
|
35
|
-
throw new Error('Start year cannot be after end year')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const startDate = new Date(startYear, 0, 1)
|
|
39
|
-
const endDate = new Date(endYear, 11, 31, 23, 59, 59, 999)
|
|
40
|
-
|
|
41
|
-
return new DateRange(startDate, endDate)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
getStartDate(): Date {
|
|
45
|
-
return new Date(this.startDate)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
getEndDate(): Date {
|
|
49
|
-
return new Date(this.endDate)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
contains(date: Date): boolean {
|
|
53
|
-
return date >= this.startDate && date <= this.endDate
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
getYearSpan(): number {
|
|
57
|
-
return this.endDate.getFullYear() - this.startDate.getFullYear() + 1
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getYears(): number[] {
|
|
61
|
-
const years: number[] = []
|
|
62
|
-
for (let year = this.startDate.getFullYear(); year <= this.endDate.getFullYear(); year++) {
|
|
63
|
-
years.push(year)
|
|
64
|
-
}
|
|
65
|
-
return years
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
equals(other: DateRange): boolean {
|
|
69
|
-
return this.startDate.getTime() === other.startDate.getTime() &&
|
|
70
|
-
this.endDate.getTime() === other.endDate.getTime()
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
toString(): string {
|
|
74
|
-
return `${this.startDate.getFullYear()}-${this.endDate.getFullYear()}`
|
|
75
|
-
}
|
|
76
|
-
}
|