commit-analyzer 1.0.2 → 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 -396
- package/src/progress.ts +0 -84
- package/src/report-generator.ts +0 -286
- package/src/types.ts +0 -24
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Analysis } from "@domain/analysis"
|
|
2
|
+
import { Commit } from "@domain/commit"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Repository interface for commit analysis operations
|
|
6
|
+
*/
|
|
7
|
+
export interface IAnalysisRepository {
|
|
8
|
+
/**
|
|
9
|
+
* Analyzes a commit and returns the analysis result
|
|
10
|
+
*/
|
|
11
|
+
analyze(commit: Commit): Promise<Analysis>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checks if the analysis service is available
|
|
15
|
+
*/
|
|
16
|
+
isAvailable(): Promise<boolean>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Gets the maximum number of retry attempts for analysis
|
|
20
|
+
*/
|
|
21
|
+
getMaxRetries(): number
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sets verbose mode for detailed error information
|
|
25
|
+
*/
|
|
26
|
+
setVerbose(verbose: boolean): void
|
|
27
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { AnalyzedCommit } from "@domain/analyzed-commit"
|
|
2
|
+
import { CommitAnalysisService } from "@domain/commit-analysis-service"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AnalyzeCommitsResult,
|
|
6
|
+
AnalyzeCommitsUseCase,
|
|
7
|
+
} from "@app/analyze-commits.usecase"
|
|
8
|
+
|
|
9
|
+
import { getErrorMessage } from "../utils"
|
|
10
|
+
|
|
11
|
+
import { ICommitRepository } from "./commit-repository.interface"
|
|
12
|
+
import { ConsoleFormatter } from "./console-formatter"
|
|
13
|
+
|
|
14
|
+
export interface AnalyzeCommandOptions {
|
|
15
|
+
commits: string[]
|
|
16
|
+
output: string
|
|
17
|
+
author?: string
|
|
18
|
+
limit?: number
|
|
19
|
+
verbose?: boolean
|
|
20
|
+
since?: string
|
|
21
|
+
until?: string
|
|
22
|
+
batchSize?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class AnalyzeCommand {
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly analyzeCommitsUseCase: AnalyzeCommitsUseCase,
|
|
28
|
+
private readonly commitAnalysisService: CommitAnalysisService,
|
|
29
|
+
private readonly commitRepository: ICommitRepository,
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
async execute(options: AnalyzeCommandOptions): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const commitHashes = await this.resolveCommitHashes(options)
|
|
35
|
+
const analysisResult = await this.performAnalysis(commitHashes, options)
|
|
36
|
+
this.displayResults(analysisResult, options.output)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
ConsoleFormatter.logError(getErrorMessage(error))
|
|
39
|
+
throw error
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async resolveCommitHashes(
|
|
44
|
+
options: AnalyzeCommandOptions,
|
|
45
|
+
): Promise<string[]> {
|
|
46
|
+
if (options.commits.length > 0) {
|
|
47
|
+
return options.commits
|
|
48
|
+
}
|
|
49
|
+
const { userEmail, userCommits } = await this.fetchUserCommits(options)
|
|
50
|
+
const commitHashes = userCommits.map((commit) =>
|
|
51
|
+
commit.getHash().getValue(),
|
|
52
|
+
)
|
|
53
|
+
if (commitHashes.length === 0) {
|
|
54
|
+
ConsoleFormatter.logWarning(`No commits found for user: ${userEmail}`)
|
|
55
|
+
throw new Error("No commits found for analysis")
|
|
56
|
+
}
|
|
57
|
+
ConsoleFormatter.logInfo(
|
|
58
|
+
`Found ${commitHashes.length} commits for user: ${userEmail}`,
|
|
59
|
+
)
|
|
60
|
+
return commitHashes
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async fetchUserCommits(options: AnalyzeCommandOptions) {
|
|
64
|
+
if (options.author) {
|
|
65
|
+
const userCommits = await this.commitRepository.getByAuthor({
|
|
66
|
+
authorEmail: options.author,
|
|
67
|
+
limit: options.limit,
|
|
68
|
+
since: options.since,
|
|
69
|
+
until: options.until,
|
|
70
|
+
})
|
|
71
|
+
return { userEmail: options.author, userCommits }
|
|
72
|
+
}
|
|
73
|
+
const userEmail = await this.commitRepository.getCurrentUserEmail()
|
|
74
|
+
const userCommits = await this.commitAnalysisService.getCurrentUserCommits({
|
|
75
|
+
limit: options.limit,
|
|
76
|
+
since: options.since,
|
|
77
|
+
until: options.until,
|
|
78
|
+
})
|
|
79
|
+
return { userEmail, userCommits }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async performAnalysis(
|
|
83
|
+
commitHashes: string[],
|
|
84
|
+
options: AnalyzeCommandOptions,
|
|
85
|
+
): Promise<AnalyzeCommitsResult> {
|
|
86
|
+
if (commitHashes.length === 0) {
|
|
87
|
+
throw new Error("No commits provided for analysis")
|
|
88
|
+
}
|
|
89
|
+
return await this.analyzeCommitsUseCase.handle({
|
|
90
|
+
commitHashes,
|
|
91
|
+
outputFile: options.output,
|
|
92
|
+
verbose: options.verbose,
|
|
93
|
+
batchSize: options.batchSize,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private displayResults(
|
|
98
|
+
result: AnalyzeCommitsResult,
|
|
99
|
+
outputFile: string,
|
|
100
|
+
): void {
|
|
101
|
+
ConsoleFormatter.logSection(
|
|
102
|
+
`Analysis complete! Results exported to ${outputFile}`,
|
|
103
|
+
)
|
|
104
|
+
ConsoleFormatter.logSuccess(
|
|
105
|
+
`Successfully analyzed ${result.analyzedCommits.length}/${result.totalProcessed} commits`,
|
|
106
|
+
)
|
|
107
|
+
if (result.failedCommits > 0) {
|
|
108
|
+
ConsoleFormatter.logWarning(
|
|
109
|
+
`Failed to analyze ${result.failedCommits} commits (see errors above)`,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
const summary = this.createAnalysisSummary(result.analyzedCommits)
|
|
113
|
+
ConsoleFormatter.displayAnalysisSummary(summary)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private createAnalysisSummary(
|
|
117
|
+
analyzedCommits: AnalyzedCommit[],
|
|
118
|
+
): Record<string, number> {
|
|
119
|
+
return analyzedCommits.reduce(
|
|
120
|
+
(summary, commit) => {
|
|
121
|
+
const category = commit.getAnalysis().getCategory().getValue()
|
|
122
|
+
summary[category] = (summary[category] || 0) + 1
|
|
123
|
+
return summary
|
|
124
|
+
},
|
|
125
|
+
{} as Record<string, number>,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Command } from "commander"
|
|
2
|
+
|
|
3
|
+
import { getErrorMessage } from "../utils"
|
|
4
|
+
|
|
5
|
+
import { CommitAnalysisController } from "./commit-analysis-controller"
|
|
6
|
+
import { ConsoleFormatter } from "./console-formatter"
|
|
7
|
+
|
|
8
|
+
export interface CLIOptions {
|
|
9
|
+
output?: string
|
|
10
|
+
outputDir?: string
|
|
11
|
+
commits: string[]
|
|
12
|
+
author?: string
|
|
13
|
+
limit?: number
|
|
14
|
+
resume?: boolean
|
|
15
|
+
clear?: boolean
|
|
16
|
+
llm?: string
|
|
17
|
+
report?: boolean
|
|
18
|
+
inputCsv?: string
|
|
19
|
+
verbose?: boolean
|
|
20
|
+
since?: string
|
|
21
|
+
until?: string
|
|
22
|
+
noCache?: boolean
|
|
23
|
+
batchSize?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class CLIApplication {
|
|
27
|
+
private static readonly VERSION = "1.1.0"
|
|
28
|
+
private static readonly DEFAULT_COMMITS_OUTPUT_FILE = "results/commits.csv"
|
|
29
|
+
private static readonly DEFAULT_REPORT_OUTPUT_FILE = "results/report.md"
|
|
30
|
+
|
|
31
|
+
constructor(private readonly controller: CommitAnalysisController) {}
|
|
32
|
+
|
|
33
|
+
async run(args: string[]): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
const program = this.createProgram()
|
|
36
|
+
await program.parseAsync(args)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
ConsoleFormatter.logError(getErrorMessage(error))
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private createProgram(): Command {
|
|
44
|
+
const program = new Command()
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.name("commit-analyzer")
|
|
48
|
+
.description(
|
|
49
|
+
"Analyze user authored git commits and generate rich commit descriptions and stakeholder reports from them.",
|
|
50
|
+
)
|
|
51
|
+
.version(CLIApplication.VERSION)
|
|
52
|
+
.option(
|
|
53
|
+
"-o, --output <file>",
|
|
54
|
+
`Output CSV file (default: ${CLIApplication.DEFAULT_COMMITS_OUTPUT_FILE})`,
|
|
55
|
+
)
|
|
56
|
+
.option(
|
|
57
|
+
"--output-dir <dir>",
|
|
58
|
+
"Output directory for CSV and report files (default: current directory)",
|
|
59
|
+
)
|
|
60
|
+
.option(
|
|
61
|
+
"-a, --author <email>",
|
|
62
|
+
"Filter commits by author email (defaults to current user)",
|
|
63
|
+
)
|
|
64
|
+
.option(
|
|
65
|
+
"-l, --limit <number>",
|
|
66
|
+
"Limit number of commits to analyze",
|
|
67
|
+
parseInt,
|
|
68
|
+
)
|
|
69
|
+
.option("-r, --resume", "Resume from last checkpoint if available")
|
|
70
|
+
.option("-c, --clear", "Clear any existing progress checkpoint")
|
|
71
|
+
.option("--llm <llm>", "LLM CLI tool to use (claude, gemini, openai)")
|
|
72
|
+
.option(
|
|
73
|
+
"--report",
|
|
74
|
+
"Generate condensed markdown report from existing CSV",
|
|
75
|
+
)
|
|
76
|
+
.option(
|
|
77
|
+
"--input-csv <file>",
|
|
78
|
+
"Input CSV file to read for report generation",
|
|
79
|
+
)
|
|
80
|
+
.option(
|
|
81
|
+
"-v, --verbose",
|
|
82
|
+
"Enable verbose logging (shows detailed error information)",
|
|
83
|
+
)
|
|
84
|
+
.option(
|
|
85
|
+
"--since <date>",
|
|
86
|
+
"Only analyze commits since this date (YYYY-MM-DD, '1 week ago', '2024-01-01')",
|
|
87
|
+
)
|
|
88
|
+
.option(
|
|
89
|
+
"--until <date>",
|
|
90
|
+
"Only analyze commits until this date (YYYY-MM-DD, '1 day ago', '2024-12-31')",
|
|
91
|
+
)
|
|
92
|
+
.option("--no-cache", "Disable caching of analysis results")
|
|
93
|
+
.option(
|
|
94
|
+
"--batch-size <number>",
|
|
95
|
+
"Number of commits to process per batch (default: 1 for sequential processing)",
|
|
96
|
+
parseInt,
|
|
97
|
+
)
|
|
98
|
+
.argument(
|
|
99
|
+
"[commits...]",
|
|
100
|
+
"Commit hashes to analyze (if none provided, uses current user's commits)",
|
|
101
|
+
)
|
|
102
|
+
.action(async (commits: string[], options: Record<string, unknown>) => {
|
|
103
|
+
const cliOptions = this.parseOptions(options, commits)
|
|
104
|
+
await this.executeCommand(cliOptions)
|
|
105
|
+
})
|
|
106
|
+
return program
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private parseOptions(
|
|
110
|
+
options: Record<string, unknown>,
|
|
111
|
+
args: string[],
|
|
112
|
+
): CLIOptions {
|
|
113
|
+
const { commits } = this.determineCommitsToAnalyze(args)
|
|
114
|
+
return {
|
|
115
|
+
output: this.determineOutputPath(
|
|
116
|
+
this.getStringOption(options.output),
|
|
117
|
+
this.getStringOption(options.outputDir),
|
|
118
|
+
),
|
|
119
|
+
outputDir: this.getStringOption(options.outputDir),
|
|
120
|
+
commits,
|
|
121
|
+
author: this.getStringOption(options.author),
|
|
122
|
+
limit: this.getNumberOption(options.limit),
|
|
123
|
+
resume: this.getBooleanOption(options.resume),
|
|
124
|
+
clear: this.getBooleanOption(options.clear),
|
|
125
|
+
llm: this.getStringOption(options.llm),
|
|
126
|
+
report: this.getBooleanOption(options.report),
|
|
127
|
+
inputCsv: this.getStringOption(options.inputCsv),
|
|
128
|
+
verbose: this.getBooleanOption(options.verbose),
|
|
129
|
+
since: this.getStringOption(options.since),
|
|
130
|
+
until: this.getStringOption(options.until),
|
|
131
|
+
noCache: this.getBooleanOption(options.noCache),
|
|
132
|
+
batchSize: this.getNumberOption(options.batchSize),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private getStringOption(value: unknown): string | undefined {
|
|
137
|
+
return typeof value === "string" ? value : undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private getNumberOption(value: unknown): number | undefined {
|
|
141
|
+
return typeof value === "number" ? value : undefined
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private getBooleanOption(value: unknown): boolean | undefined {
|
|
145
|
+
return typeof value === "boolean" ? value : undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private async executeCommand(options: CLIOptions): Promise<void> {
|
|
149
|
+
// Handle clear flag
|
|
150
|
+
if (options.clear) {
|
|
151
|
+
await this.controller.handleClearProgress()
|
|
152
|
+
if (!options.resume) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Handle input CSV mode (report generation only)
|
|
158
|
+
if (options.inputCsv) {
|
|
159
|
+
await this.controller.handleReportGeneration({
|
|
160
|
+
inputCsv: options.inputCsv,
|
|
161
|
+
output: options.output || CLIApplication.DEFAULT_REPORT_OUTPUT_FILE,
|
|
162
|
+
sourceInfo: { type: "csv", value: options.inputCsv },
|
|
163
|
+
})
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Handle resume mode
|
|
168
|
+
if (options.resume) {
|
|
169
|
+
const resumed = await this.controller.handleResumeAnalysis({
|
|
170
|
+
verbose: options.verbose,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
if (resumed && options.report) {
|
|
174
|
+
const reportOutput = options.output
|
|
175
|
+
? this.getReportOutputPath(options.output)
|
|
176
|
+
: CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
|
|
177
|
+
|
|
178
|
+
await this.controller.handleReportGeneration({
|
|
179
|
+
inputCsv: options.output,
|
|
180
|
+
output: reportOutput,
|
|
181
|
+
sourceInfo: {
|
|
182
|
+
type: "csv",
|
|
183
|
+
value: options.output || CLIApplication.DEFAULT_COMMITS_OUTPUT_FILE,
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Handle normal analysis mode
|
|
191
|
+
const analyzeOptions = {
|
|
192
|
+
commits: options.commits,
|
|
193
|
+
output: options.output!,
|
|
194
|
+
author: options.author,
|
|
195
|
+
limit: options.limit,
|
|
196
|
+
verbose: options.verbose,
|
|
197
|
+
since: options.since,
|
|
198
|
+
until: options.until,
|
|
199
|
+
batchSize: options.batchSize,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (options.report) {
|
|
203
|
+
// Analysis + Report workflow
|
|
204
|
+
const reportOutput = options.output
|
|
205
|
+
? this.getReportOutputPath(options.output)
|
|
206
|
+
: CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
|
|
207
|
+
|
|
208
|
+
await this.controller.handleAnalysisWithReport(analyzeOptions, {
|
|
209
|
+
output: reportOutput,
|
|
210
|
+
sourceInfo:
|
|
211
|
+
options.commits.length > 0
|
|
212
|
+
? { type: "commits", value: options.commits.join(",") }
|
|
213
|
+
: { type: "author", value: options.author || "current user" },
|
|
214
|
+
})
|
|
215
|
+
} else {
|
|
216
|
+
// Analysis only workflow
|
|
217
|
+
await this.controller.handleAnalysis(analyzeOptions)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private determineCommitsToAnalyze(args: string[]): { commits: string[] } {
|
|
222
|
+
let commits: string[] = []
|
|
223
|
+
if (args.length > 0) {
|
|
224
|
+
commits = args
|
|
225
|
+
}
|
|
226
|
+
return { commits }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private determineOutputPath(
|
|
230
|
+
outputOption?: string,
|
|
231
|
+
outputDir?: string,
|
|
232
|
+
): string {
|
|
233
|
+
if (outputOption) {
|
|
234
|
+
return outputOption
|
|
235
|
+
}
|
|
236
|
+
if (outputDir) {
|
|
237
|
+
return `${outputDir}/commits.csv`
|
|
238
|
+
}
|
|
239
|
+
return CLIApplication.DEFAULT_COMMITS_OUTPUT_FILE
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private getReportOutputPath(csvPath: string): string {
|
|
243
|
+
// If no specific output path provided, use the default report path
|
|
244
|
+
if (!csvPath) {
|
|
245
|
+
return CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Extract directory from CSV path and use report.md as filename
|
|
249
|
+
if (csvPath.endsWith(".csv")) {
|
|
250
|
+
const dir = csvPath.substring(0, csvPath.lastIndexOf("/"))
|
|
251
|
+
return dir ? `${dir}/report.md` : "report.md"
|
|
252
|
+
}
|
|
253
|
+
return csvPath + ".md"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { CacheService } from "@infra/cache-service"
|
|
2
|
+
|
|
3
|
+
import { AnalyzeCommand, AnalyzeCommandOptions } from "./analyze-command"
|
|
4
|
+
import { ICommitRepository } from "./commit-repository.interface"
|
|
5
|
+
import { ConsoleFormatter } from "./console-formatter"
|
|
6
|
+
import { IProgressRepository } from "./progress-repository.interface"
|
|
7
|
+
import { ReportCommand, ReportCommandOptions } from "./report-command"
|
|
8
|
+
import { ResumeCommand, ResumeCommandOptions } from "./resume-command"
|
|
9
|
+
|
|
10
|
+
export class CommitAnalysisController {
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly analyzeCommand: AnalyzeCommand,
|
|
13
|
+
private readonly reportCommand: ReportCommand,
|
|
14
|
+
private readonly resumeCommand: ResumeCommand,
|
|
15
|
+
private readonly progressRepository: IProgressRepository,
|
|
16
|
+
private readonly cacheService: CacheService,
|
|
17
|
+
private readonly commitRepository: ICommitRepository,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handles the main analysis workflow
|
|
22
|
+
*/
|
|
23
|
+
async handleAnalysis(options: AnalyzeCommandOptions): Promise<void> {
|
|
24
|
+
await this.analyzeCommand.execute(options)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handles report generation
|
|
29
|
+
*/
|
|
30
|
+
async handleReportGeneration(options: ReportCommandOptions): Promise<void> {
|
|
31
|
+
await this.reportCommand.execute(options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handles resuming analysis from checkpoint
|
|
36
|
+
*/
|
|
37
|
+
async handleResumeAnalysis(options: ResumeCommandOptions): Promise<boolean> {
|
|
38
|
+
return this.resumeCommand.execute(options)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Handles the combined analysis and report workflow
|
|
43
|
+
*/
|
|
44
|
+
async handleAnalysisWithReport(
|
|
45
|
+
analyzeOptions: AnalyzeCommandOptions,
|
|
46
|
+
reportOptions: ReportCommandOptions,
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
// First run analysis
|
|
49
|
+
await this.handleAnalysis(analyzeOptions)
|
|
50
|
+
|
|
51
|
+
// Resolve source info with actual user email if needed
|
|
52
|
+
const resolvedSourceInfo = await this.resolveSourceInfo(
|
|
53
|
+
reportOptions.sourceInfo,
|
|
54
|
+
analyzeOptions.author,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// Then generate report using the analysis output
|
|
58
|
+
const reportOptionsWithInput: ReportCommandOptions = {
|
|
59
|
+
...reportOptions,
|
|
60
|
+
inputCsv: analyzeOptions.output,
|
|
61
|
+
sourceInfo: resolvedSourceInfo,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await this.handleReportGeneration(reportOptionsWithInput)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Handles clearing progress and cache
|
|
69
|
+
*/
|
|
70
|
+
async handleClearProgress(): Promise<void> {
|
|
71
|
+
await this.progressRepository.clearProgress()
|
|
72
|
+
await this.cacheService.clear()
|
|
73
|
+
ConsoleFormatter.logSuccess("✓ Progress checkpoint and cache cleared")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolves source info with actual user email when needed
|
|
78
|
+
*/
|
|
79
|
+
private async resolveSourceInfo(
|
|
80
|
+
sourceInfo?: { type: "author" | "commits" | "csv"; value: string },
|
|
81
|
+
authorOption?: string,
|
|
82
|
+
): Promise<
|
|
83
|
+
{ type: "author" | "commits" | "csv"; value: string } | undefined
|
|
84
|
+
> {
|
|
85
|
+
if (!sourceInfo) {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If source is author and value is 'current user', get actual email
|
|
90
|
+
if (sourceInfo.type === "author" && sourceInfo.value === "current user") {
|
|
91
|
+
const actualEmail =
|
|
92
|
+
authorOption || (await this.commitRepository.getCurrentUserEmail())
|
|
93
|
+
return {
|
|
94
|
+
type: "author",
|
|
95
|
+
value: actualEmail,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return sourceInfo
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Commit } from "@domain/commit"
|
|
2
|
+
import { CommitHash } from "@domain/commit-hash"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Repository interface for accessing commit data
|
|
6
|
+
*/
|
|
7
|
+
export interface ICommitRepository {
|
|
8
|
+
/**
|
|
9
|
+
* Retrieves a commit by its hash
|
|
10
|
+
*/
|
|
11
|
+
getByHash(hash: CommitHash): Promise<Commit>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Retrieves commits authored by a specific user
|
|
15
|
+
*/
|
|
16
|
+
getByAuthor(params: {
|
|
17
|
+
authorEmail: string
|
|
18
|
+
limit?: number
|
|
19
|
+
since?: string
|
|
20
|
+
until?: string
|
|
21
|
+
}): Promise<Commit[]>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Retrieves commits from a list of hashes
|
|
25
|
+
*/
|
|
26
|
+
getByHashes(hashes: CommitHash[]): Promise<Commit[]>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates if a commit hash exists in the repository
|
|
30
|
+
*/
|
|
31
|
+
exists(hash: CommitHash): Promise<boolean>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Gets the current user's email from the repository configuration
|
|
35
|
+
*/
|
|
36
|
+
getCurrentUserEmail(): Promise<string>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the current user's name from the repository configuration
|
|
40
|
+
*/
|
|
41
|
+
getCurrentUserName(): Promise<string>
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if the current directory is a valid repository
|
|
45
|
+
*/
|
|
46
|
+
isValidRepository(): Promise<boolean>
|
|
47
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console formatter for consistent output styling
|
|
3
|
+
*/
|
|
4
|
+
export class ConsoleFormatter {
|
|
5
|
+
/**
|
|
6
|
+
* Log a success message with checkmark
|
|
7
|
+
*/
|
|
8
|
+
static logSuccess(message: string): void {
|
|
9
|
+
console.log(`✓ ${message}`)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Log an error message with X mark
|
|
14
|
+
*/
|
|
15
|
+
static logError(message: string): void {
|
|
16
|
+
console.error(`❌ ${message}`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Log a warning message with warning icon
|
|
21
|
+
*/
|
|
22
|
+
static logWarning(message: string): void {
|
|
23
|
+
console.log(`⚠️ ${message}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Log a debug message for development purposes
|
|
28
|
+
*/
|
|
29
|
+
static logDebug(message: string): void {
|
|
30
|
+
console.log(`🐛 ${message}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Log an info message with bullet point
|
|
35
|
+
*/
|
|
36
|
+
static logInfo(message: string): void {
|
|
37
|
+
console.log(` - ${message}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Log a progress message with arrow
|
|
42
|
+
*/
|
|
43
|
+
static logProgress(message: string): void {
|
|
44
|
+
console.log(`▶️ ${message}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Log a completion message with celebration icon
|
|
49
|
+
*/
|
|
50
|
+
static logComplete(message: string): void {
|
|
51
|
+
console.log(`🎉 ${message}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Log a file operation with folder icon
|
|
56
|
+
*/
|
|
57
|
+
static logFile(message: string): void {
|
|
58
|
+
console.log(`📁 ${message}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Log a save operation with disk icon
|
|
63
|
+
*/
|
|
64
|
+
static logSave(message: string): void {
|
|
65
|
+
console.log(`💾 ${message}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Log a report generation with chart icon
|
|
70
|
+
*/
|
|
71
|
+
static logReport(message: string): void {
|
|
72
|
+
console.log(`📊 ${message}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Log with indentation for nested information
|
|
77
|
+
*/
|
|
78
|
+
static logIndented(message: string, level: number = 1): void {
|
|
79
|
+
const indent = ' '.repeat(level)
|
|
80
|
+
console.log(`${indent}${message}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Log a section header with newlines for spacing
|
|
85
|
+
*/
|
|
86
|
+
static logSection(title: string): void {
|
|
87
|
+
console.log(`\n${title}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Log with custom emoji/icon
|
|
92
|
+
*/
|
|
93
|
+
static logWithIcon(icon: string, message: string): void {
|
|
94
|
+
console.log(`${icon} ${message}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Display analysis summary in a formatted way
|
|
99
|
+
*/
|
|
100
|
+
static displayAnalysisSummary(summary: Record<string, number>): void {
|
|
101
|
+
this.logSection("Summary by category:")
|
|
102
|
+
Object.entries(summary).forEach(([category, count]) => {
|
|
103
|
+
this.logInfo(`${category}: ${count} commits`)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Update progress bar inline
|
|
109
|
+
*/
|
|
110
|
+
static updateProgress(current: number, total: number, description?: string): void {
|
|
111
|
+
const percentage = Math.round((current / total) * 100)
|
|
112
|
+
const completed = Math.round((current / total) * 20) // 20 characters for progress bar
|
|
113
|
+
const remaining = 20 - completed
|
|
114
|
+
|
|
115
|
+
const progressBar = '█'.repeat(completed) + '░'.repeat(remaining)
|
|
116
|
+
const desc = description ? ` ${description}` : ''
|
|
117
|
+
|
|
118
|
+
// Use \r to overwrite the line
|
|
119
|
+
process.stdout.write(`\r▶️ [${progressBar}] ${percentage}% (${current}/${total})${desc}`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Complete progress and clear the line
|
|
124
|
+
*/
|
|
125
|
+
static completeProgress(): void {
|
|
126
|
+
// Clear the current line and move to beginning
|
|
127
|
+
process.stdout.write('\r\x1b[K')
|
|
128
|
+
}
|
|
129
|
+
}
|