@manojkmfsi/monodog 1.0.20 → 1.0.21
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/config-loader.js +5 -6
- package/dist/get-db-url.js +0 -1
- package/dist/index.js +219 -80
- package/dist/serve.js +7 -91
- package/dist/services/commitService.js +1 -1
- package/dist/{gitService.js → services/gitService.js} +0 -101
- package/dist/utils/utilities.js +92 -14
- package/{monodog-conf.json → monodog-config.json} +2 -4
- package/monodog-dashboard/dist/assets/{index-dadb5f0d.js → index-1a6836e4.js} +2 -2
- package/monodog-dashboard/dist/index.html +1 -1
- package/package.json +4 -3
- package/src/config-loader.ts +5 -7
- package/src/get-db-url.ts +0 -2
- package/src/index.ts +227 -72
- package/src/serve.ts +6 -68
- package/src/services/commitService.ts +1 -1
- package/src/services/gitService.ts +165 -0
- package/src/utils/utilities.ts +105 -14
- package/src/gitService.ts +0 -276
- /package/{monodog-conf.example.json → monodog-config.example.json} +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitService.ts
|
|
3
|
+
*
|
|
4
|
+
* This service executes native 'git' commands using Node.js's child_process
|
|
5
|
+
* to retrieve the commit history of the local repository.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { exec } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
// Promisify the standard 'exec' function for easy async/await usage
|
|
13
|
+
const execPromise = promisify(exec);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Interface representing a single commit object with key metadata.
|
|
17
|
+
*/
|
|
18
|
+
interface GitCommit {
|
|
19
|
+
hash: string;
|
|
20
|
+
author: string;
|
|
21
|
+
packageName: string;
|
|
22
|
+
date: Date;
|
|
23
|
+
message: string;
|
|
24
|
+
type: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* List of standard Conventional Commit types for validation.
|
|
28
|
+
* Any extracted type not in this list will be set to 'other'.
|
|
29
|
+
*/
|
|
30
|
+
const VALID_COMMIT_TYPES: string[] = [
|
|
31
|
+
'feat', // New feature
|
|
32
|
+
'fix', // Bug fix
|
|
33
|
+
'docs', // Documentation changes
|
|
34
|
+
'style', // Code style changes (formatting, etc)
|
|
35
|
+
'refactor', // Code refactoring
|
|
36
|
+
'perf', // Performance improvements
|
|
37
|
+
'test', // Adding or updating tests
|
|
38
|
+
'chore', // Maintenance tasks (e.g., build scripts, dependency updates)
|
|
39
|
+
'ci', // CI/CD changes
|
|
40
|
+
'build', // Build system changes (e.g., pnpm/npm scripts)
|
|
41
|
+
'revert', // Reverting changes
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export class GitService {
|
|
45
|
+
private repoPath: string;
|
|
46
|
+
|
|
47
|
+
constructor(repoPath: string = process.cwd()) {
|
|
48
|
+
this.repoPath = repoPath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Retrieves commit history for the repository, optionally filtered by a package path.
|
|
53
|
+
*/
|
|
54
|
+
public async getAllCommits(pathFilter?: string): Promise<GitCommit[]> {
|
|
55
|
+
try {
|
|
56
|
+
|
|
57
|
+
let pathArgument = '';
|
|
58
|
+
if (pathFilter) {
|
|
59
|
+
// Normalize the path and ensure it's relative to the repo root
|
|
60
|
+
const normalizedPath = this.normalizePath(pathFilter);
|
|
61
|
+
if (normalizedPath) {
|
|
62
|
+
pathArgument = ` -C ${normalizedPath}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// First, validate we're in a git repo
|
|
67
|
+
await this.validateGitRepository(pathArgument);
|
|
68
|
+
|
|
69
|
+
console.log(`🔧 Executing Git command in: ${this.repoPath}`);
|
|
70
|
+
// Use a simpler git log format
|
|
71
|
+
const command = `git log --pretty=format:"%H|%an|%ad|%s" --date=iso-strict ${pathArgument}`;
|
|
72
|
+
console.log(`📝 Git command: ${command}`);
|
|
73
|
+
|
|
74
|
+
const { stdout, stderr } = await execPromise(command, {
|
|
75
|
+
cwd: this.repoPath,
|
|
76
|
+
maxBuffer: 1024 * 5000,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (stderr) {
|
|
80
|
+
console.warn('Git stderr:', stderr);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!stdout.trim()) {
|
|
84
|
+
console.log('📭 No commits found for path:', pathFilter);
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Parse the output
|
|
89
|
+
const commits: GitCommit[] = [];
|
|
90
|
+
const lines = stdout.trim().split('\n');
|
|
91
|
+
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
try {
|
|
94
|
+
const [hash, author, date, message] = line.split('|');
|
|
95
|
+
|
|
96
|
+
const commit: GitCommit = {
|
|
97
|
+
hash: hash.trim(),
|
|
98
|
+
author: author.trim(),
|
|
99
|
+
packageName: pathFilter || 'root',
|
|
100
|
+
date: new Date(date.trim()),
|
|
101
|
+
message: message.trim(),
|
|
102
|
+
type: this.extractCommitType(message.trim()),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
commits.push(commit);
|
|
106
|
+
} catch (parseError) {
|
|
107
|
+
console.error('❌ Failed to parse commit line:', line, parseError);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`✅ Successfully parsed ${commits.length} commits`);
|
|
112
|
+
return commits;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('💥 Error in getAllCommits:', error);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Normalize path to be relative to git repo root
|
|
121
|
+
*/
|
|
122
|
+
private normalizePath(inputPath: string): string {
|
|
123
|
+
// If it's an absolute path, make it relative to repo root
|
|
124
|
+
if (path.isAbsolute(inputPath)) {
|
|
125
|
+
return path.relative(this.repoPath, inputPath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// If it's already relative, return as-is
|
|
129
|
+
return inputPath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract commit type from message
|
|
134
|
+
*/
|
|
135
|
+
private extractCommitType(message: string): string {
|
|
136
|
+
try {
|
|
137
|
+
const typeMatch = message.match(/^(\w+)(\([^)]+\))?!?:/);
|
|
138
|
+
if (typeMatch) {
|
|
139
|
+
const rawType = typeMatch[1].toLowerCase();
|
|
140
|
+
if (VALID_COMMIT_TYPES.includes(rawType)) {
|
|
141
|
+
return rawType;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return 'other';
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return 'other';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validate that we're in a git repository
|
|
152
|
+
*/
|
|
153
|
+
private async validateGitRepository(pathArgument: string): Promise<void> {
|
|
154
|
+
try {
|
|
155
|
+
await execPromise('git '+pathArgument+' rev-parse --is-inside-work-tree', {
|
|
156
|
+
cwd: this.repoPath,
|
|
157
|
+
});
|
|
158
|
+
console.log('✅ Valid git repository');
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
'Not a git repository (or any of the parent directories)'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/utils/utilities.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { appConfig } from '../config-loader';
|
|
5
5
|
import {calculatePackageHealth} from './health-utils';
|
|
6
|
+
import * as yaml from 'js-yaml';
|
|
6
7
|
|
|
7
8
|
export {PackageHealth} from './health-utils';
|
|
8
9
|
|
|
@@ -73,30 +74,72 @@ export function resolveWorkspaceGlobs(rootDir: string, globs: string[]): string[
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
|
-
*
|
|
77
|
+
* Parses pnpm-workspace.yaml and extracts workspace globs
|
|
77
78
|
*/
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
if (!fs.existsSync(
|
|
81
|
-
console.warn(`\n⚠️ Warning: No package.json found at root directory: ${rootDir}`);
|
|
79
|
+
function getWorkspacesFromPnpmYaml(rootDir: string): string[] | undefined {
|
|
80
|
+
const workspaceYamlPath = path.join(rootDir, 'pnpm-workspace.yaml');
|
|
81
|
+
if (!fs.existsSync(workspaceYamlPath)) {
|
|
82
82
|
return undefined;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
try {
|
|
86
|
-
const
|
|
86
|
+
const yamlContent = fs.readFileSync(workspaceYamlPath, 'utf-8');
|
|
87
|
+
const yamlData = yaml.load(yamlContent) as { packages?: (string | string[]) };
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
if (yamlData && yamlData.packages) {
|
|
90
|
+
// Filter out exclusion patterns (lines starting with '!')
|
|
91
|
+
const packages = Array.isArray(yamlData.packages)
|
|
92
|
+
? yamlData.packages.filter((pkg) => typeof pkg === 'string' && !pkg.startsWith('!'))
|
|
93
|
+
: [];
|
|
94
|
+
|
|
95
|
+
if (packages.length > 0) {
|
|
96
|
+
return packages;
|
|
97
|
+
}
|
|
93
98
|
}
|
|
94
99
|
} catch (e) {
|
|
95
|
-
console.error(`\n❌ Error parsing
|
|
100
|
+
console.error(`\n❌ Error parsing pnpm-workspace.yaml at ${workspaceYamlPath}:`, e);
|
|
96
101
|
}
|
|
97
102
|
return undefined;
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Reads workspace configuration from package.json or pnpm-workspace.yaml
|
|
107
|
+
* Priority: package.json (if exists) -> pnpm-workspace.yaml
|
|
108
|
+
*/
|
|
109
|
+
export function getWorkspacesFromRoot(rootDir: string): string[] | undefined {
|
|
110
|
+
const packageJsonPath = path.join(rootDir, 'package.json');
|
|
111
|
+
|
|
112
|
+
// Try package.json first
|
|
113
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
114
|
+
try {
|
|
115
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
116
|
+
|
|
117
|
+
// Handle both standard array and object format (used by yarn/pnpm)
|
|
118
|
+
if (Array.isArray(packageJson.workspaces)) {
|
|
119
|
+
console.log('✅ Workspace configuration found in package.json');
|
|
120
|
+
return packageJson.workspaces;
|
|
121
|
+
} else if (packageJson.workspaces && Array.isArray(packageJson.workspaces.packages)) {
|
|
122
|
+
console.log('✅ Workspace configuration found in package.json');
|
|
123
|
+
return packageJson.workspaces.packages;
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error(`\n❌ Error parsing package.json at ${packageJsonPath}. Attempting to read pnpm-workspace.yaml...`);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
console.warn(`\n⚠️ Warning: No package.json found at root directory: ${rootDir}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fallback to pnpm-workspace.yaml
|
|
133
|
+
const pnpmWorkspaces = getWorkspacesFromPnpmYaml(rootDir);
|
|
134
|
+
if (pnpmWorkspaces && pnpmWorkspaces.length > 0) {
|
|
135
|
+
console.log('✅ Workspace configuration found in pnpm-workspace.yaml');
|
|
136
|
+
return pnpmWorkspaces;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.warn('\n⚠️ No workspace configuration found in package.json or pnpm-workspace.yaml');
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
/**
|
|
101
144
|
* Scans the monorepo and returns information about all packages
|
|
102
145
|
*/
|
|
@@ -104,7 +147,7 @@ function scanMonorepo(rootDir: string): PackageInfo[] {
|
|
|
104
147
|
const packages: PackageInfo[] = [];
|
|
105
148
|
console.log('rootDir:', rootDir);
|
|
106
149
|
const workspacesGlobs = appConfig.workspaces;
|
|
107
|
-
// Use provided workspaces globs if given, otherwise attempt to detect from root package.json
|
|
150
|
+
// Use provided workspaces globs if given, otherwise attempt to detect from root package.json or pnpm-workspace.yaml
|
|
108
151
|
const detectedWorkspacesGlobs = workspacesGlobs.length > 0 ? workspacesGlobs : getWorkspacesFromRoot(rootDir);
|
|
109
152
|
if (detectedWorkspacesGlobs && detectedWorkspacesGlobs.length > 0) {
|
|
110
153
|
if (workspacesGlobs.length) {
|
|
@@ -356,6 +399,53 @@ function getPackageSize(packagePath: string): {
|
|
|
356
399
|
}
|
|
357
400
|
}
|
|
358
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Find the monorepo root by looking for package.json with workspaces or pnpm-workspace.yaml
|
|
404
|
+
*/
|
|
405
|
+
function findMonorepoRoot(): string {
|
|
406
|
+
let currentDir = __dirname;
|
|
407
|
+
|
|
408
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
409
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
410
|
+
const pnpmWorkspacePath = path.join(currentDir, 'pnpm-workspace.yaml');
|
|
411
|
+
|
|
412
|
+
// Check if this directory has package.json with workspaces or pnpm-workspace.yaml
|
|
413
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
414
|
+
try {
|
|
415
|
+
const packageJson = JSON.parse(
|
|
416
|
+
fs.readFileSync(packageJsonPath, 'utf8')
|
|
417
|
+
);
|
|
418
|
+
// If it has workspaces or is the root monorepo package
|
|
419
|
+
if (packageJson.workspaces || fs.existsSync(pnpmWorkspacePath)) {
|
|
420
|
+
console.log('✅ Found monorepo root:', currentDir);
|
|
421
|
+
return currentDir;
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
// Continue searching if package.json is invalid
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Check if we're at the git root
|
|
429
|
+
const gitPath = path.join(currentDir, '.git');
|
|
430
|
+
if (fs.existsSync(gitPath)) {
|
|
431
|
+
console.log('✅ Found git root (likely monorepo root):', currentDir);
|
|
432
|
+
return currentDir;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Go up one directory
|
|
436
|
+
const parentDir = path.dirname(currentDir);
|
|
437
|
+
if (parentDir === currentDir) break; // Prevent infinite loop
|
|
438
|
+
currentDir = parentDir;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Fallback to process.cwd() if we can't find the root
|
|
442
|
+
console.log(
|
|
443
|
+
'⚠️ Could not find monorepo root, using process.cwd():',
|
|
444
|
+
process.cwd()
|
|
445
|
+
);
|
|
446
|
+
return process.cwd();
|
|
447
|
+
}
|
|
448
|
+
|
|
359
449
|
export {
|
|
360
450
|
scanMonorepo,
|
|
361
451
|
generateMonorepoStats,
|
|
@@ -363,5 +453,6 @@ export {
|
|
|
363
453
|
generateDependencyGraph,
|
|
364
454
|
checkOutdatedDependencies,
|
|
365
455
|
getPackageSize,
|
|
366
|
-
calculatePackageHealth
|
|
456
|
+
calculatePackageHealth,
|
|
457
|
+
findMonorepoRoot,
|
|
367
458
|
};
|
package/src/gitService.ts
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitService.ts
|
|
3
|
-
*
|
|
4
|
-
* This service executes native 'git' commands using Node.js's child_process
|
|
5
|
-
* to retrieve the commit history of the local repository.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { exec } from 'child_process';
|
|
9
|
-
import { promisify } from 'util';
|
|
10
|
-
import path from 'path';
|
|
11
|
-
|
|
12
|
-
// Promisify the standard 'exec' function for easy async/await usage
|
|
13
|
-
const execPromise = promisify(exec);
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Interface representing a single commit object with key metadata.
|
|
17
|
-
*/
|
|
18
|
-
interface GitCommit {
|
|
19
|
-
hash: string;
|
|
20
|
-
author: string;
|
|
21
|
-
packageName: string;
|
|
22
|
-
date: Date;
|
|
23
|
-
message: string;
|
|
24
|
-
type: string;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* List of standard Conventional Commit types for validation.
|
|
28
|
-
* Any extracted type not in this list will be set to 'other'.
|
|
29
|
-
*/
|
|
30
|
-
const VALID_COMMIT_TYPES: string[] = [
|
|
31
|
-
'feat', // New feature
|
|
32
|
-
'fix', // Bug fix
|
|
33
|
-
'docs', // Documentation changes
|
|
34
|
-
'style', // Code style changes (formatting, etc)
|
|
35
|
-
'refactor', // Code refactoring
|
|
36
|
-
'perf', // Performance improvements
|
|
37
|
-
'test', // Adding or updating tests
|
|
38
|
-
'chore', // Maintenance tasks (e.g., build scripts, dependency updates)
|
|
39
|
-
'ci', // CI/CD changes
|
|
40
|
-
'build', // Build system changes (e.g., pnpm/npm scripts)
|
|
41
|
-
'revert', // Reverting changes
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The delimiter used to separate individual commit records in the git log output.
|
|
46
|
-
* We use a unique string to ensure reliable splitting.
|
|
47
|
-
*/
|
|
48
|
-
// const COMMIT_DELIMITER = '|---COMMIT-END---|';
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The custom format string passed to 'git log'.
|
|
52
|
-
* It uses JSON structure and our unique delimiter to make parsing consistent.
|
|
53
|
-
* * %H: Commit hash
|
|
54
|
-
* %pn: packageName name
|
|
55
|
-
* %ae: Author email
|
|
56
|
-
* %ad: Author date (ISO 8601 format)
|
|
57
|
-
* %s: Subject (commit message's first line - used for 'message' and 'type' fields)
|
|
58
|
-
*/
|
|
59
|
-
// const GIT_LOG_FORMAT = `{"hash": "%H", "author": "%ae", "packageName": "%pn", "date": "%ad", "message": "%s", "type": "%s"}${COMMIT_DELIMITER}`;
|
|
60
|
-
|
|
61
|
-
// export class GitService {
|
|
62
|
-
// private repoPath: string;
|
|
63
|
-
|
|
64
|
-
// constructor(repoPath: string = process.cwd()) {
|
|
65
|
-
// // Allows specifying a custom path to the monorepo root
|
|
66
|
-
// this.repoPath = repoPath;
|
|
67
|
-
// }
|
|
68
|
-
|
|
69
|
-
// /**
|
|
70
|
-
// * Retrieves commit history for the repository, optionally filtered by a package path.
|
|
71
|
-
// * @param pathFilter The path to a package directory (e.g., 'packages/server').
|
|
72
|
-
// * If provided, only commits affecting that path are returned.
|
|
73
|
-
// * @returns A Promise resolving to an array of GitCommit objects.
|
|
74
|
-
// */
|
|
75
|
-
// public async getAllCommits(pathFilter?: string): Promise<GitCommit[]> {
|
|
76
|
-
// const pathArgument = pathFilter ? ` -- ${pathFilter}` : '';
|
|
77
|
-
|
|
78
|
-
// const command = `git log --pretty=format:'${GIT_LOG_FORMAT}' --date=iso-strict${pathArgument}`;
|
|
79
|
-
|
|
80
|
-
// try {
|
|
81
|
-
// console.log(`Executing Git command: ${command}`);
|
|
82
|
-
// const { stdout } = await execPromise(command, {
|
|
83
|
-
// cwd: this.repoPath,
|
|
84
|
-
// maxBuffer: 1024 * 5000,
|
|
85
|
-
// }); // Increase buffer for large repos
|
|
86
|
-
// // console.log(stdout)
|
|
87
|
-
// console.log(stdout, '--', this.repoPath);
|
|
88
|
-
// const escapedDelimiter = COMMIT_DELIMITER.replace(
|
|
89
|
-
// /[.*+?^${}()|[\]\\]/g,
|
|
90
|
-
// '\\$&'
|
|
91
|
-
// );
|
|
92
|
-
|
|
93
|
-
// // 1. Remove the final trailing delimiter, as it creates an empty string at the end of the array
|
|
94
|
-
// const cleanedOutput = stdout
|
|
95
|
-
// .trim()
|
|
96
|
-
// .replace(new RegExp(`${escapedDelimiter}$`), '');
|
|
97
|
-
// console.log(cleanedOutput);
|
|
98
|
-
|
|
99
|
-
// if (!cleanedOutput) {
|
|
100
|
-
// return [];
|
|
101
|
-
// }
|
|
102
|
-
// // 2. Split the output by our custom delimiter to get an array of JSON strings
|
|
103
|
-
// const jsonStrings = cleanedOutput.split(COMMIT_DELIMITER);
|
|
104
|
-
// // 3. Parse each string into a GitCommit object
|
|
105
|
-
// const commits: GitCommit[] = jsonStrings
|
|
106
|
-
// .map(jsonString => {
|
|
107
|
-
// try {
|
|
108
|
-
// // JSON.parse is necessary because the output starts as a string
|
|
109
|
-
// const commit = JSON.parse(jsonString);
|
|
110
|
-
// console.log(commit.type, commit['hash']);
|
|
111
|
-
// try {
|
|
112
|
-
// // FIX: Updated Regex to handle optional scope (e.g., 'feat(scope):')
|
|
113
|
-
// // Matches:
|
|
114
|
-
// // 1. (^(\w+)) - The commit type (e.g., 'feat')
|
|
115
|
-
// // 2. (\([^)]+\))? - The optional scope (e.g., '(setup)')
|
|
116
|
-
// // 3. (:|!) - Ends with a colon or an exclamation point (for breaking changes)
|
|
117
|
-
// const typeMatch = commit.type.match(/^(\w+)(\([^)]+\))?([:!])/);
|
|
118
|
-
// let extractedType = 'other';
|
|
119
|
-
// if (typeMatch) {
|
|
120
|
-
// const rawType = typeMatch[1];
|
|
121
|
-
// // NEW: Validate the extracted type against the list of known types
|
|
122
|
-
// if (VALID_COMMIT_TYPES.includes(rawType)) {
|
|
123
|
-
// extractedType = rawType;
|
|
124
|
-
// }
|
|
125
|
-
// }
|
|
126
|
-
// commit.type = extractedType;
|
|
127
|
-
// } catch (e) {
|
|
128
|
-
// console.error('Failed to match commit type:', commit, e);
|
|
129
|
-
// // Skip malformed entries
|
|
130
|
-
// return null;
|
|
131
|
-
// }
|
|
132
|
-
// // // Set type to 'other' if the conventional format is not found
|
|
133
|
-
// // commit.type = typeMatch ? typeMatch[1] : 'other';
|
|
134
|
-
// return commit;
|
|
135
|
-
// } catch (e) {
|
|
136
|
-
// // console.log(jsonString)
|
|
137
|
-
// console.error('Failed to parse commit JSON:', jsonString, e);
|
|
138
|
-
// // Skip malformed entries
|
|
139
|
-
// return null;
|
|
140
|
-
// }
|
|
141
|
-
// })
|
|
142
|
-
// .filter((commit): commit is GitCommit => commit !== null);
|
|
143
|
-
|
|
144
|
-
// return commits;
|
|
145
|
-
// } catch (error) {
|
|
146
|
-
// console.error('Error fetching Git history:', error);
|
|
147
|
-
// // In a real application, you would handle this gracefully (e.g., throwing a custom error)
|
|
148
|
-
// throw new Error(
|
|
149
|
-
// 'Failed to retrieve Git commit history. Is Git installed and is the path correct?'
|
|
150
|
-
// );
|
|
151
|
-
// }
|
|
152
|
-
// }
|
|
153
|
-
// }
|
|
154
|
-
|
|
155
|
-
export class GitService {
|
|
156
|
-
private repoPath: string;
|
|
157
|
-
|
|
158
|
-
constructor(repoPath: string = process.cwd()) {
|
|
159
|
-
this.repoPath = repoPath;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Retrieves commit history for the repository, optionally filtered by a package path.
|
|
164
|
-
*/
|
|
165
|
-
public async getAllCommits(pathFilter?: string): Promise<GitCommit[]> {
|
|
166
|
-
try {
|
|
167
|
-
|
|
168
|
-
let pathArgument = '';
|
|
169
|
-
if (pathFilter) {
|
|
170
|
-
// Normalize the path and ensure it's relative to the repo root
|
|
171
|
-
const normalizedPath = this.normalizePath(pathFilter);
|
|
172
|
-
if (normalizedPath) {
|
|
173
|
-
pathArgument = ` -C ${normalizedPath}`;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// First, validate we're in a git repo
|
|
178
|
-
await this.validateGitRepository(pathArgument);
|
|
179
|
-
|
|
180
|
-
console.log(`🔧 Executing Git command in: ${this.repoPath}`);
|
|
181
|
-
// Use a simpler git log format
|
|
182
|
-
const command = `git log --pretty=format:"%H|%an|%ad|%s" --date=iso-strict ${pathArgument}`;
|
|
183
|
-
console.log(`📝 Git command: ${command}`);
|
|
184
|
-
|
|
185
|
-
const { stdout, stderr } = await execPromise(command, {
|
|
186
|
-
cwd: this.repoPath,
|
|
187
|
-
maxBuffer: 1024 * 5000,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
if (stderr) {
|
|
191
|
-
console.warn('Git stderr:', stderr);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (!stdout.trim()) {
|
|
195
|
-
console.log('📭 No commits found for path:', pathFilter);
|
|
196
|
-
return [];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Parse the output
|
|
200
|
-
const commits: GitCommit[] = [];
|
|
201
|
-
const lines = stdout.trim().split('\n');
|
|
202
|
-
|
|
203
|
-
for (const line of lines) {
|
|
204
|
-
try {
|
|
205
|
-
const [hash, author, date, message] = line.split('|');
|
|
206
|
-
|
|
207
|
-
const commit: GitCommit = {
|
|
208
|
-
hash: hash.trim(),
|
|
209
|
-
author: author.trim(),
|
|
210
|
-
packageName: pathFilter || 'root',
|
|
211
|
-
date: new Date(date.trim()),
|
|
212
|
-
message: message.trim(),
|
|
213
|
-
type: this.extractCommitType(message.trim()),
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
commits.push(commit);
|
|
217
|
-
} catch (parseError) {
|
|
218
|
-
console.error('❌ Failed to parse commit line:', line, parseError);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
console.log(`✅ Successfully parsed ${commits.length} commits`);
|
|
223
|
-
return commits;
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.error('💥 Error in getAllCommits:', error);
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Normalize path to be relative to git repo root
|
|
232
|
-
*/
|
|
233
|
-
private normalizePath(inputPath: string): string {
|
|
234
|
-
// If it's an absolute path, make it relative to repo root
|
|
235
|
-
if (path.isAbsolute(inputPath)) {
|
|
236
|
-
return path.relative(this.repoPath, inputPath);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// If it's already relative, return as-is
|
|
240
|
-
return inputPath;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Extract commit type from message
|
|
245
|
-
*/
|
|
246
|
-
private extractCommitType(message: string): string {
|
|
247
|
-
try {
|
|
248
|
-
const typeMatch = message.match(/^(\w+)(\([^)]+\))?!?:/);
|
|
249
|
-
if (typeMatch) {
|
|
250
|
-
const rawType = typeMatch[1].toLowerCase();
|
|
251
|
-
if (VALID_COMMIT_TYPES.includes(rawType)) {
|
|
252
|
-
return rawType;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return 'other';
|
|
256
|
-
} catch (error) {
|
|
257
|
-
return 'other';
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Validate that we're in a git repository
|
|
263
|
-
*/
|
|
264
|
-
private async validateGitRepository(pathArgument: string): Promise<void> {
|
|
265
|
-
try {
|
|
266
|
-
await execPromise('git '+pathArgument+' rev-parse --is-inside-work-tree', {
|
|
267
|
-
cwd: this.repoPath,
|
|
268
|
-
});
|
|
269
|
-
console.log('✅ Valid git repository');
|
|
270
|
-
} catch (error) {
|
|
271
|
-
throw new Error(
|
|
272
|
-
'Not a git repository (or any of the parent directories)'
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
File without changes
|