e-pick 2.0.0 → 3.0.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.md +339 -0
- package/README.md +109 -0
- package/package.json +39 -10
- package/public/css/styles.css +994 -0
- package/public/index.html +216 -34
- package/public/js/api-facade.js +113 -0
- package/public/js/app.js +285 -0
- package/public/js/cherry-pick-builder.js +165 -0
- package/public/js/commit-validator.js +183 -0
- package/public/js/file-parser.js +30 -0
- package/public/js/filter-strategy.js +225 -0
- package/public/js/observable.js +113 -0
- package/public/js/parsers/base-parser.js +92 -0
- package/public/js/parsers/csv-parser.js +88 -0
- package/public/js/parsers/excel-parser.js +142 -0
- package/public/js/parsers/parser-factory.js +69 -0
- package/public/js/stepper-states.js +319 -0
- package/public/js/ui-manager.js +668 -0
- package/src/cli.js +289 -0
- package/src/commands/cherry-pick.command.js +79 -0
- package/src/config/app.config.js +115 -0
- package/src/config/repo-manager.js +131 -0
- package/src/controllers/commit.controller.js +102 -0
- package/src/middleware/error.middleware.js +33 -0
- package/src/middleware/validation.middleware.js +61 -0
- package/src/server.js +121 -0
- package/src/services/git.service.js +277 -0
- package/src/services/validation.service.js +102 -0
- package/src/utils/error-handler.js +80 -0
- package/src/validators/commit.validator.js +160 -0
- package/cli.js +0 -111
- package/lib/pick-commit.js +0 -165
- package/public/script.js +0 -263
- package/public/styles.css +0 -179
- package/server.js +0 -154
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handling Middleware
|
|
3
|
+
*
|
|
4
|
+
* Global error handler for Express
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { serializeError } from '../utils/error-handler.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Global error handling middleware
|
|
11
|
+
* Must be registered last in middleware chain
|
|
12
|
+
*/
|
|
13
|
+
export function errorHandler(err, req, res, next) {
|
|
14
|
+
console.error('Error:', err);
|
|
15
|
+
|
|
16
|
+
const serialized = serializeError(err);
|
|
17
|
+
|
|
18
|
+
res.status(serialized.statusCode || 500).json({
|
|
19
|
+
success: false,
|
|
20
|
+
...serialized,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 404 Not Found handler
|
|
26
|
+
*/
|
|
27
|
+
export function notFoundHandler(req, res) {
|
|
28
|
+
res.status(404).json({
|
|
29
|
+
success: false,
|
|
30
|
+
error: 'NotFound',
|
|
31
|
+
message: `Cannot ${req.method} ${req.path}`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Middleware
|
|
3
|
+
*
|
|
4
|
+
* Request validation using Joi schemas
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Joi from 'joi';
|
|
8
|
+
import { ValidationError } from '../utils/error-handler.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate request body against Joi schema
|
|
12
|
+
* @param {object} schema - Joi schema
|
|
13
|
+
* @returns {Function} Express middleware
|
|
14
|
+
*/
|
|
15
|
+
export function validateBody(schema) {
|
|
16
|
+
return (req, res, next) => {
|
|
17
|
+
const { error, value } = schema.validate(req.body, {
|
|
18
|
+
abortEarly: false,
|
|
19
|
+
stripUnknown: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (error) {
|
|
23
|
+
const message = error.details.map((d) => d.message).join(', ');
|
|
24
|
+
return next(new ValidationError(message));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
req.body = value;
|
|
28
|
+
next();
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate commits array schema
|
|
34
|
+
*/
|
|
35
|
+
export const validateCommitsSchema = Joi.object({
|
|
36
|
+
commits: Joi.array()
|
|
37
|
+
.items(Joi.string().pattern(/^[0-9a-f]{40}$/i))
|
|
38
|
+
.min(1)
|
|
39
|
+
.required()
|
|
40
|
+
.messages({
|
|
41
|
+
'array.min': 'At least one commit is required',
|
|
42
|
+
'string.pattern.base': 'Each commit must be a valid 40-character hash',
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate command schema
|
|
48
|
+
*/
|
|
49
|
+
export const generateCommandSchema = Joi.object({
|
|
50
|
+
commits: Joi.array()
|
|
51
|
+
.items(Joi.string().pattern(/^[0-9a-f]{40}$/i))
|
|
52
|
+
.min(1)
|
|
53
|
+
.required(),
|
|
54
|
+
ignoredCommits: Joi.array()
|
|
55
|
+
.items(Joi.string().pattern(/^[0-9a-f]{40}$/i))
|
|
56
|
+
.default([]),
|
|
57
|
+
options: Joi.object({
|
|
58
|
+
allowEmpty: Joi.boolean().default(false),
|
|
59
|
+
continueOnConflict: Joi.boolean().default(false),
|
|
60
|
+
}).default({}),
|
|
61
|
+
});
|
package/src/server.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express Server
|
|
3
|
+
*
|
|
4
|
+
* Main server file that sets up Express app and routes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import helmet from 'helmet';
|
|
9
|
+
import cors from 'cors';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { dirname, join } from 'path';
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
import appConfig from './config/app.config.js';
|
|
14
|
+
import * as commitController from './controllers/commit.controller.js';
|
|
15
|
+
import { validateBody, validateCommitsSchema, generateCommandSchema } from './middleware/validation.middleware.js';
|
|
16
|
+
import { errorHandler, notFoundHandler } from './middleware/error.middleware.js';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start the Express server
|
|
23
|
+
* @param {object} options - Server options
|
|
24
|
+
* @param {string} options.repoPath - Path to git repository
|
|
25
|
+
* @param {number} options.port - Port to run server on
|
|
26
|
+
* @returns {Promise<{server: object, actualPort: number}>}
|
|
27
|
+
*/
|
|
28
|
+
export async function startServer(options) {
|
|
29
|
+
const { repoPath, port } = options;
|
|
30
|
+
|
|
31
|
+
// Set repo path in config
|
|
32
|
+
appConfig.setRepoPath(repoPath);
|
|
33
|
+
|
|
34
|
+
// Create Express app
|
|
35
|
+
const app = express();
|
|
36
|
+
|
|
37
|
+
// Security middleware
|
|
38
|
+
app.use(helmet({
|
|
39
|
+
contentSecurityPolicy: false, // Allow inline scripts for CDN libraries
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// CORS middleware
|
|
43
|
+
app.use(cors(appConfig.getCorsConfig()));
|
|
44
|
+
|
|
45
|
+
// Parse JSON bodies
|
|
46
|
+
app.use(express.json({ limit: '10mb' }));
|
|
47
|
+
|
|
48
|
+
// Serve static files
|
|
49
|
+
const publicPath = join(__dirname, '../public');
|
|
50
|
+
app.use(express.static(publicPath));
|
|
51
|
+
|
|
52
|
+
// API Routes
|
|
53
|
+
|
|
54
|
+
// Health check endpoint
|
|
55
|
+
app.get('/api/health', (req, res) => {
|
|
56
|
+
res.json({
|
|
57
|
+
status: 'ok',
|
|
58
|
+
repoPath: appConfig.getRepoPath(),
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Get version from package.json
|
|
63
|
+
app.get('/api/version', (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const packageJsonPath = join(__dirname, '../package.json');
|
|
66
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
67
|
+
res.json({
|
|
68
|
+
version: packageJson.version,
|
|
69
|
+
name: packageJson.name,
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
res.status(500).json({ error: 'Failed to read version' });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Get repository information
|
|
77
|
+
app.get('/api/repo-info', commitController.getRepoInfo);
|
|
78
|
+
|
|
79
|
+
// Check if commit exists
|
|
80
|
+
app.get('/api/check-commit/:commitHash', commitController.checkCommit);
|
|
81
|
+
|
|
82
|
+
// Validate commits
|
|
83
|
+
app.post(
|
|
84
|
+
'/api/validate',
|
|
85
|
+
validateBody(validateCommitsSchema),
|
|
86
|
+
commitController.validateCommits,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Generate cherry-pick command
|
|
90
|
+
app.post(
|
|
91
|
+
'/api/generate-command',
|
|
92
|
+
validateBody(generateCommandSchema),
|
|
93
|
+
commitController.generateCommand,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// 404 handler
|
|
97
|
+
app.use(notFoundHandler);
|
|
98
|
+
|
|
99
|
+
// Error handling middleware (must be last)
|
|
100
|
+
app.use(errorHandler);
|
|
101
|
+
|
|
102
|
+
// Start server with port fallback logic
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const tryPort = (portToTry) => {
|
|
105
|
+
const server = app.listen(portToTry, () => {
|
|
106
|
+
resolve({ server, actualPort: portToTry });
|
|
107
|
+
}).on('error', (err) => {
|
|
108
|
+
if (err.code === 'EADDRINUSE') {
|
|
109
|
+
// Port is in use, try random port in fallback range
|
|
110
|
+
const { min, max } = appConfig.getServerConfig().fallbackPortRange;
|
|
111
|
+
const randomPort = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
112
|
+
tryPort(randomPort);
|
|
113
|
+
} else {
|
|
114
|
+
reject(err);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
tryPort(port);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Service (Repository Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Abstracts all git operations and provides a clean interface
|
|
5
|
+
* for interacting with git repositories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import appConfig from '../config/app.config.js';
|
|
10
|
+
|
|
11
|
+
class GitService {
|
|
12
|
+
constructor(repoPath) {
|
|
13
|
+
this.repoPath = repoPath || appConfig.getRepoPath();
|
|
14
|
+
this.timeout = appConfig.getGitConfig().timeout;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute a git command
|
|
19
|
+
* @param {string} command - Git command to execute
|
|
20
|
+
* @returns {string} Command output
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
_execGit(command) {
|
|
24
|
+
try {
|
|
25
|
+
return execSync(command, {
|
|
26
|
+
cwd: this.repoPath,
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
timeout: this.timeout,
|
|
29
|
+
stdio: 'pipe',
|
|
30
|
+
}).trim();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`Git command failed: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a path is a valid git repository (static method)
|
|
38
|
+
* @param {string} path - Path to check
|
|
39
|
+
* @returns {boolean} True if valid git repo
|
|
40
|
+
*/
|
|
41
|
+
static checkIfGitRepo(path) {
|
|
42
|
+
try {
|
|
43
|
+
execSync('git rev-parse --git-dir', {
|
|
44
|
+
cwd: path,
|
|
45
|
+
stdio: 'pipe',
|
|
46
|
+
});
|
|
47
|
+
return true;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate if a commit hash exists in the repository
|
|
55
|
+
* @param {string} commitHash - Commit hash to validate
|
|
56
|
+
* @returns {boolean} True if commit exists
|
|
57
|
+
*/
|
|
58
|
+
validateCommit(commitHash) {
|
|
59
|
+
try {
|
|
60
|
+
const result = this._execGit(`git cat-file -t ${commitHash}`);
|
|
61
|
+
return result === 'commit';
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get detailed information about a commit
|
|
69
|
+
* @param {string} commitHash - Commit hash
|
|
70
|
+
* @returns {object|null} Commit information or null if not found
|
|
71
|
+
*/
|
|
72
|
+
getCommitInfo(commitHash) {
|
|
73
|
+
try {
|
|
74
|
+
// Get commit details in a specific format
|
|
75
|
+
const format = '%ci|%an|%ae|%s|%H';
|
|
76
|
+
const output = this._execGit(`git show -s --format="${format}" ${commitHash}`);
|
|
77
|
+
|
|
78
|
+
const [date, author, email, message, hash] = output.split('|');
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
hash,
|
|
82
|
+
date: new Date(date).toISOString(),
|
|
83
|
+
author,
|
|
84
|
+
email,
|
|
85
|
+
message,
|
|
86
|
+
shortHash: hash.substring(0, 7),
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get current branch name
|
|
95
|
+
* @returns {string} Branch name
|
|
96
|
+
*/
|
|
97
|
+
getCurrentBranch() {
|
|
98
|
+
try {
|
|
99
|
+
return this._execGit('git rev-parse --abbrev-ref HEAD');
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return 'unknown';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get repository name/identifier
|
|
107
|
+
* @returns {string} Repository name
|
|
108
|
+
*/
|
|
109
|
+
getRepoName() {
|
|
110
|
+
try {
|
|
111
|
+
// Try to get from remote URL first
|
|
112
|
+
const remoteUrl = this._execGit('git config --get remote.origin.url');
|
|
113
|
+
const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/);
|
|
114
|
+
if (match) {
|
|
115
|
+
return match[1];
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// No remote, use directory name
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback to directory name
|
|
122
|
+
return this.repoPath.split('/').filter(Boolean).pop();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get repository information
|
|
127
|
+
* @returns {object} Repository information
|
|
128
|
+
*/
|
|
129
|
+
getRepoInfo() {
|
|
130
|
+
try {
|
|
131
|
+
const branch = this.getCurrentBranch();
|
|
132
|
+
const lastCommit = this._execGit('git log -1 --format="%H|%s|%ci"');
|
|
133
|
+
const [hash, message, date] = lastCommit.split('|');
|
|
134
|
+
|
|
135
|
+
let remoteName = 'No remote';
|
|
136
|
+
let remoteUrl = null;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
remoteUrl = this._execGit('git config --get remote.origin.url');
|
|
140
|
+
// Extract repo name from URL
|
|
141
|
+
const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/);
|
|
142
|
+
if (match) {
|
|
143
|
+
remoteName = match[1];
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// No remote configured
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
path: this.repoPath,
|
|
151
|
+
branch,
|
|
152
|
+
remoteName,
|
|
153
|
+
remoteUrl,
|
|
154
|
+
repoName: this.getRepoName(),
|
|
155
|
+
lastCommit: {
|
|
156
|
+
hash: hash.substring(0, 7),
|
|
157
|
+
message,
|
|
158
|
+
date: new Date(date).toISOString(),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new Error(`Failed to get repository info: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Sort commits by date (chronological order)
|
|
168
|
+
* @param {string[]} commits - Array of commit hashes
|
|
169
|
+
* @returns {string[]} Sorted commit hashes
|
|
170
|
+
*/
|
|
171
|
+
sortCommitsByDate(commits) {
|
|
172
|
+
try {
|
|
173
|
+
if (commits.length === 0) return [];
|
|
174
|
+
|
|
175
|
+
// Get commit dates
|
|
176
|
+
const command = `git show -s --format="%ci %H" ${commits.join(' ')}`;
|
|
177
|
+
const output = this._execGit(command);
|
|
178
|
+
|
|
179
|
+
// Parse and sort by date
|
|
180
|
+
const commitData = output
|
|
181
|
+
.split('\n')
|
|
182
|
+
.map((line) => {
|
|
183
|
+
const parts = line.split(' ');
|
|
184
|
+
const date = parts.slice(0, 3).join(' ');
|
|
185
|
+
const hash = parts[3];
|
|
186
|
+
return { date, hash };
|
|
187
|
+
})
|
|
188
|
+
.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
189
|
+
|
|
190
|
+
return commitData.map((item) => item.hash);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new Error(`Failed to sort commits: ${error.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Generate cherry-pick command
|
|
198
|
+
* @param {string[]} commits - Array of commit hashes (will be sorted)
|
|
199
|
+
* @param {object} options - Options for cherry-pick
|
|
200
|
+
* @returns {string} Cherry-pick command
|
|
201
|
+
*/
|
|
202
|
+
generateCherryPickCommand(commits, options = {}) {
|
|
203
|
+
if (commits.length === 0) {
|
|
204
|
+
throw new Error('No commits provided');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Sort commits chronologically
|
|
208
|
+
const sortedCommits = this.sortCommitsByDate(commits);
|
|
209
|
+
|
|
210
|
+
// Build command
|
|
211
|
+
let command = 'git cherry-pick';
|
|
212
|
+
|
|
213
|
+
if (options.allowEmpty) {
|
|
214
|
+
command += ' --allow-empty';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options.continueOnConflict) {
|
|
218
|
+
command += ' --continue';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
command += ` ${sortedCommits.join(' ')}`;
|
|
222
|
+
|
|
223
|
+
return command;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Execute cherry-pick command (optional - for Phase 6)
|
|
228
|
+
* @param {string[]} commits - Array of commit hashes
|
|
229
|
+
* @param {object} options - Options for cherry-pick
|
|
230
|
+
* @returns {object} Execution result
|
|
231
|
+
*/
|
|
232
|
+
executeCherryPick(commits, options = {}) {
|
|
233
|
+
try {
|
|
234
|
+
const command = this.generateCherryPickCommand(commits, options);
|
|
235
|
+
const output = this._execGit(command);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
output,
|
|
240
|
+
command,
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
error: error.message,
|
|
246
|
+
command: this.generateCherryPickCommand(commits, options),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get list of files changed in commits
|
|
253
|
+
* @param {string[]} commits - Array of commit hashes
|
|
254
|
+
* @returns {string[]} Array of unique file paths
|
|
255
|
+
*/
|
|
256
|
+
getChangedFiles(commits) {
|
|
257
|
+
try {
|
|
258
|
+
if (commits.length === 0) return [];
|
|
259
|
+
|
|
260
|
+
// Get list of files changed in all commits using git show
|
|
261
|
+
const command = `git show --name-only --pretty=format: ${commits.join(' ')}`;
|
|
262
|
+
const output = this._execGit(command);
|
|
263
|
+
|
|
264
|
+
// Split by newline and remove duplicates
|
|
265
|
+
const files = output
|
|
266
|
+
.split('\n')
|
|
267
|
+
.filter((file) => file.trim() !== '')
|
|
268
|
+
.filter((file, index, self) => self.indexOf(file) === index);
|
|
269
|
+
|
|
270
|
+
return files;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
throw new Error(`Failed to get changed files: ${error.message}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default GitService;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Service
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates validation of multiple commits and provides
|
|
5
|
+
* summary and grouping functionality.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { validateCommit } from '../validators/commit.validator.js';
|
|
9
|
+
import GitService from './git.service.js';
|
|
10
|
+
|
|
11
|
+
class ValidationService {
|
|
12
|
+
constructor(repoPath) {
|
|
13
|
+
this.gitService = new GitService(repoPath);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validate multiple commits
|
|
18
|
+
* @param {string[]} commits - Array of commit hashes
|
|
19
|
+
* @returns {Promise<object>} Validation results
|
|
20
|
+
*/
|
|
21
|
+
async validateCommits(commits) {
|
|
22
|
+
if (!Array.isArray(commits) || commits.length === 0) {
|
|
23
|
+
return {
|
|
24
|
+
results: [],
|
|
25
|
+
summary: {
|
|
26
|
+
total: 0,
|
|
27
|
+
valid: 0,
|
|
28
|
+
invalid: 0,
|
|
29
|
+
canBeIgnored: 0,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate each commit
|
|
35
|
+
const validationPromises = commits.map((commit) => validateCommit(commit, { gitService: this.gitService }));
|
|
36
|
+
|
|
37
|
+
const results = await Promise.all(validationPromises);
|
|
38
|
+
|
|
39
|
+
// Generate summary
|
|
40
|
+
const summary = this.generateSummary(results);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
results,
|
|
44
|
+
summary,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate validation summary
|
|
50
|
+
* @param {array} results - Validation results
|
|
51
|
+
* @returns {object} Summary statistics
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
generateSummary(results) {
|
|
55
|
+
const summary = {
|
|
56
|
+
total: results.length,
|
|
57
|
+
valid: 0,
|
|
58
|
+
invalid: 0,
|
|
59
|
+
canBeIgnored: 0,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
results.forEach((result) => {
|
|
63
|
+
if (result.isValid) {
|
|
64
|
+
summary.valid++;
|
|
65
|
+
} else {
|
|
66
|
+
summary.invalid++;
|
|
67
|
+
if (result.canIgnore) {
|
|
68
|
+
summary.canBeIgnored++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return summary;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Group validation results by status
|
|
78
|
+
* @param {array} results - Validation results
|
|
79
|
+
* @returns {object} Grouped results
|
|
80
|
+
*/
|
|
81
|
+
groupValidationResults(results) {
|
|
82
|
+
return {
|
|
83
|
+
valid: results.filter((r) => r.isValid),
|
|
84
|
+
invalid: results.filter((r) => !r.isValid && !r.canIgnore),
|
|
85
|
+
ignorable: results.filter((r) => !r.isValid && r.canIgnore),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Filter commits by validity
|
|
91
|
+
* @param {array} results - Validation results
|
|
92
|
+
* @param {array} ignoredCommits - Commits to ignore (optional)
|
|
93
|
+
* @returns {array} Valid commit hashes
|
|
94
|
+
*/
|
|
95
|
+
getValidCommits(results, ignoredCommits = []) {
|
|
96
|
+
return results
|
|
97
|
+
.filter((r) => r.isValid && !ignoredCommits.includes(r.commit))
|
|
98
|
+
.map((r) => r.commit);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default ValidationService;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Utilities
|
|
3
|
+
*
|
|
4
|
+
* Custom error classes and error handling utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base application error class
|
|
9
|
+
*/
|
|
10
|
+
export class AppError extends Error {
|
|
11
|
+
constructor(message, statusCode = 500) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.isOperational = true;
|
|
15
|
+
Error.captureStackTrace(this, this.constructor);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validation error (400)
|
|
21
|
+
*/
|
|
22
|
+
export class ValidationError extends AppError {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message, 400);
|
|
25
|
+
this.name = 'ValidationError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Git operation error (500)
|
|
31
|
+
*/
|
|
32
|
+
export class GitError extends AppError {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message, 500);
|
|
35
|
+
this.name = 'GitError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Not found error (404)
|
|
41
|
+
*/
|
|
42
|
+
export class NotFoundError extends AppError {
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message, 404);
|
|
45
|
+
this.name = 'NotFoundError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Serialize error for API response
|
|
51
|
+
* @param {Error} error - Error object
|
|
52
|
+
* @returns {object} Serialized error
|
|
53
|
+
*/
|
|
54
|
+
export function serializeError(error) {
|
|
55
|
+
if (error instanceof AppError) {
|
|
56
|
+
return {
|
|
57
|
+
error: error.name,
|
|
58
|
+
message: error.message,
|
|
59
|
+
statusCode: error.statusCode,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Unknown error
|
|
64
|
+
return {
|
|
65
|
+
error: 'InternalServerError',
|
|
66
|
+
message: 'An unexpected error occurred',
|
|
67
|
+
statusCode: 500,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Async handler wrapper to catch errors in async route handlers
|
|
73
|
+
* @param {Function} fn - Async function
|
|
74
|
+
* @returns {Function} Wrapped function
|
|
75
|
+
*/
|
|
76
|
+
export function asyncHandler(fn) {
|
|
77
|
+
return (req, res, next) => {
|
|
78
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
79
|
+
};
|
|
80
|
+
}
|