e-pick 1.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 +38 -8
- package/public/css/styles.css +994 -0
- package/public/index.html +226 -0
- 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 -81
- package/lib/pick-commit.js +0 -162
package/src/cli.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI Entry Point for E-Pick Tool
|
|
5
|
+
*
|
|
6
|
+
* Provides a command-line interface for starting the web server
|
|
7
|
+
* with support for repository management via subcommands.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import open from 'open';
|
|
13
|
+
import { resolve, isAbsolute, dirname } from 'path';
|
|
14
|
+
import { existsSync, statSync } from 'fs';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import appConfig from './config/app.config.js';
|
|
18
|
+
import repoManager from './config/repo-manager.js';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
|
|
23
|
+
const program = new Command();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a path is a valid git repository
|
|
27
|
+
* @param {string} path - Path to check
|
|
28
|
+
* @returns {boolean} True if valid git repo
|
|
29
|
+
*/
|
|
30
|
+
function isGitRepository(path) {
|
|
31
|
+
try {
|
|
32
|
+
execSync('git rev-parse --git-dir', {
|
|
33
|
+
cwd: path,
|
|
34
|
+
stdio: 'pipe',
|
|
35
|
+
});
|
|
36
|
+
return true;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate repository path
|
|
44
|
+
* @param {string} path - Path to validate
|
|
45
|
+
* @returns {string} Absolute path if valid
|
|
46
|
+
* @throws {Error} If path is invalid
|
|
47
|
+
*/
|
|
48
|
+
function validateRepoPath(path) {
|
|
49
|
+
const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
|
|
50
|
+
|
|
51
|
+
if (!existsSync(absolutePath)) {
|
|
52
|
+
throw new Error(`Path does not exist: ${absolutePath}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const stats = statSync(absolutePath);
|
|
56
|
+
if (!stats.isDirectory()) {
|
|
57
|
+
throw new Error(`Path is not a directory: ${absolutePath}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!isGitRepository(absolutePath)) {
|
|
61
|
+
throw new Error(`Path is not a git repository: ${absolutePath}\n\nPlease initialize git or specify a valid git repository.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return absolutePath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get repository name from path
|
|
69
|
+
* @param {string} repoPath - Repository path
|
|
70
|
+
* @returns {string} Repository name
|
|
71
|
+
*/
|
|
72
|
+
function getRepoName(repoPath) {
|
|
73
|
+
try {
|
|
74
|
+
const remoteName = execSync('git config --get remote.origin.url', {
|
|
75
|
+
cwd: repoPath,
|
|
76
|
+
stdio: 'pipe',
|
|
77
|
+
encoding: 'utf-8',
|
|
78
|
+
}).trim();
|
|
79
|
+
|
|
80
|
+
const match = remoteName.match(/\/([^\/]+?)(?:\.git)?$/);
|
|
81
|
+
if (match) {
|
|
82
|
+
return match[1];
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// No remote, use directory name
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return repoPath.split('/').filter(Boolean).pop();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Display startup banner
|
|
93
|
+
*/
|
|
94
|
+
function displayBanner(repoPath, repoName, port) {
|
|
95
|
+
console.log();
|
|
96
|
+
console.log(chalk.cyan.bold('🚀 E-Pick Server'));
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(chalk.gray('Repository:'), chalk.white.bold(repoName));
|
|
99
|
+
console.log(chalk.gray('Path:'), chalk.white(repoPath));
|
|
100
|
+
console.log(chalk.gray('Server:'), chalk.green.bold(`http://localhost:${port}`));
|
|
101
|
+
console.log();
|
|
102
|
+
console.log(chalk.yellow('Press Ctrl+C to stop'));
|
|
103
|
+
console.log();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Main CLI function
|
|
108
|
+
*/
|
|
109
|
+
async function main() {
|
|
110
|
+
// ====================
|
|
111
|
+
// REPO SUBCOMMAND
|
|
112
|
+
// ====================
|
|
113
|
+
const repoCmd = program
|
|
114
|
+
.command('repo')
|
|
115
|
+
.description('Manage saved repositories');
|
|
116
|
+
|
|
117
|
+
// repo list
|
|
118
|
+
repoCmd
|
|
119
|
+
.command('list')
|
|
120
|
+
.description('List all saved repositories')
|
|
121
|
+
.action(() => {
|
|
122
|
+
const repos = repoManager.listRepos();
|
|
123
|
+
const entries = Object.entries(repos);
|
|
124
|
+
|
|
125
|
+
if (entries.length === 0) {
|
|
126
|
+
console.log(chalk.yellow('No saved repositories'));
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log();
|
|
131
|
+
console.log(chalk.cyan.bold('📁 Saved Repositories'));
|
|
132
|
+
console.log();
|
|
133
|
+
|
|
134
|
+
entries.forEach(([alias, path]) => {
|
|
135
|
+
console.log(chalk.white.bold(alias), chalk.gray('→'), chalk.gray(path));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log();
|
|
139
|
+
console.log(chalk.gray(`Total: ${entries.length} repositor${entries.length === 1 ? 'y' : 'ies'}`));
|
|
140
|
+
console.log();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// repo add
|
|
144
|
+
repoCmd
|
|
145
|
+
.command('add <alias> [path]')
|
|
146
|
+
.description('Save a repository with an alias')
|
|
147
|
+
.action((alias, path) => {
|
|
148
|
+
const repoPath = path || process.cwd();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const absolutePath = validateRepoPath(repoPath);
|
|
152
|
+
repoManager.saveRepo(alias, absolutePath);
|
|
153
|
+
|
|
154
|
+
console.log(chalk.green(`✓ Saved repository: ${alias}`));
|
|
155
|
+
console.log(chalk.gray(` Path: ${absolutePath}`));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(chalk.red('✗'), error.message);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// repo remove
|
|
163
|
+
repoCmd
|
|
164
|
+
.command('remove <alias>')
|
|
165
|
+
.description('Remove a saved repository')
|
|
166
|
+
.action((alias) => {
|
|
167
|
+
const success = repoManager.removeRepo(alias);
|
|
168
|
+
|
|
169
|
+
if (success) {
|
|
170
|
+
console.log(chalk.green(`✓ Removed repository: ${alias}`));
|
|
171
|
+
} else {
|
|
172
|
+
console.log(chalk.red(`✗ Repository not found: ${alias}`));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
// ====================
|
|
179
|
+
// MAIN COMMAND - Start Server
|
|
180
|
+
// ====================
|
|
181
|
+
program
|
|
182
|
+
.name('epick')
|
|
183
|
+
.description('Web-based tool for cherry-picking git commits from Excel/CSV files')
|
|
184
|
+
.version('3.0.0')
|
|
185
|
+
.argument('[repository]', 'Repository alias or path (default: current directory)')
|
|
186
|
+
.option('-p, --port <number>', 'Port to run server on (default: 7991)', parseInt)
|
|
187
|
+
.option('--no-open', 'Do not automatically open browser')
|
|
188
|
+
.action(async (repository, options) => {
|
|
189
|
+
let repoPath = repository || process.cwd();
|
|
190
|
+
let resolvedFromAlias = false;
|
|
191
|
+
|
|
192
|
+
// Check if repository is an alias
|
|
193
|
+
if (repository) {
|
|
194
|
+
const savedPath = repoManager.getRepoPath(repository);
|
|
195
|
+
if (savedPath) {
|
|
196
|
+
repoPath = savedPath;
|
|
197
|
+
resolvedFromAlias = true;
|
|
198
|
+
console.log(chalk.gray(`Using saved repository: ${repository}`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Validate repository
|
|
203
|
+
console.log(chalk.gray('Validating repository...'));
|
|
204
|
+
|
|
205
|
+
let validatedPath;
|
|
206
|
+
try {
|
|
207
|
+
validatedPath = validateRepoPath(repoPath);
|
|
208
|
+
appConfig.setRepoPath(validatedPath);
|
|
209
|
+
console.log(chalk.green('✓ Repository validated'));
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error();
|
|
212
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
213
|
+
console.error();
|
|
214
|
+
|
|
215
|
+
if (error.message.includes('not a git repository')) {
|
|
216
|
+
console.error(chalk.gray('Tip: Make sure you are in a git repository or provide a valid path'));
|
|
217
|
+
console.error(chalk.gray('Example: epick /path/to/your/repo'));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Get repository name
|
|
224
|
+
const repoName = getRepoName(validatedPath);
|
|
225
|
+
|
|
226
|
+
// Set port
|
|
227
|
+
const port = options.port || appConfig.getServerConfig().defaultPort;
|
|
228
|
+
|
|
229
|
+
// Start server
|
|
230
|
+
console.log(chalk.gray('Starting server...'));
|
|
231
|
+
|
|
232
|
+
const { startServer } = await import('./server.js');
|
|
233
|
+
const { server, actualPort } = await startServer({
|
|
234
|
+
repoPath: validatedPath,
|
|
235
|
+
port,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
console.log(chalk.green(`✓ Server started on port ${actualPort}`));
|
|
239
|
+
|
|
240
|
+
// Auto-save repository if not already saved
|
|
241
|
+
if (!resolvedFromAlias && repository && repository !== process.cwd()) {
|
|
242
|
+
repoManager.saveRepo(getRepoName(validatedPath), validatedPath);
|
|
243
|
+
console.log(chalk.gray(`Auto-saved as: ${getRepoName(validatedPath)}`));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Display banner
|
|
247
|
+
displayBanner(validatedPath, repoName, actualPort);
|
|
248
|
+
|
|
249
|
+
// Auto-open browser
|
|
250
|
+
if (options.open !== false) {
|
|
251
|
+
try {
|
|
252
|
+
await open(`http://localhost:${actualPort}`);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.log(chalk.gray('Note: Could not auto-open browser'));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Handle graceful shutdown
|
|
259
|
+
const shutdown = async () => {
|
|
260
|
+
console.log();
|
|
261
|
+
console.log(chalk.yellow('Shutting down server...'));
|
|
262
|
+
|
|
263
|
+
if (server) {
|
|
264
|
+
server.close(() => {
|
|
265
|
+
console.log(chalk.green('Server stopped'));
|
|
266
|
+
process.exit(0);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
console.log(chalk.red('Forcing shutdown...'));
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}, 5000);
|
|
273
|
+
} else {
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
process.on('SIGINT', shutdown);
|
|
279
|
+
process.on('SIGTERM', shutdown);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await program.parseAsync(process.argv);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Run CLI
|
|
286
|
+
main().catch((error) => {
|
|
287
|
+
console.error(chalk.red('Fatal error:'), error.message);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cherry-Pick Command (Command Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates cherry-pick operations as command objects.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class CherryPickCommand {
|
|
8
|
+
constructor(gitService, commits, options = {}) {
|
|
9
|
+
this.gitService = gitService;
|
|
10
|
+
this.commits = commits;
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute the cherry-pick command
|
|
16
|
+
* @returns {object} Execution result
|
|
17
|
+
*/
|
|
18
|
+
execute() {
|
|
19
|
+
try {
|
|
20
|
+
// Generate the command
|
|
21
|
+
const command = this.gitService.generateCherryPickCommand(
|
|
22
|
+
this.commits,
|
|
23
|
+
this.options,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Get sorted commits with info
|
|
27
|
+
const sortedCommits = this.gitService.sortCommitsByDate(this.commits);
|
|
28
|
+
const commitsWithInfo = sortedCommits.map((hash) => {
|
|
29
|
+
const info = this.gitService.getCommitInfo(hash);
|
|
30
|
+
return info || { hash, message: 'Unknown', date: null };
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Get list of changed files
|
|
34
|
+
const changedFiles = this.gitService.getChangedFiles(this.commits);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
command,
|
|
39
|
+
commits: commitsWithInfo,
|
|
40
|
+
changedFiles,
|
|
41
|
+
warning: this._generateWarnings(),
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
error: error.message,
|
|
47
|
+
command: null,
|
|
48
|
+
commits: [],
|
|
49
|
+
changedFiles: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate warnings if needed
|
|
56
|
+
* @returns {string|null} Warning message
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
_generateWarnings() {
|
|
60
|
+
if (this.commits.length > 50) {
|
|
61
|
+
return `You are cherry-picking ${this.commits.length} commits. This may take a while and could result in conflicts.`;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Undo the command (for future implementation)
|
|
68
|
+
* @returns {object} Undo result
|
|
69
|
+
*/
|
|
70
|
+
undo() {
|
|
71
|
+
// Future: Implement cherry-pick abort
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
message: 'Undo not yet implemented',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default CherryPickCommand;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Configuration (Singleton Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Centralized configuration management for the application.
|
|
5
|
+
* Ensures a single source of truth for all configuration values.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class AppConfig {
|
|
9
|
+
constructor() {
|
|
10
|
+
if (AppConfig.instance) {
|
|
11
|
+
return AppConfig.instance;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Server configuration
|
|
15
|
+
this.server = {
|
|
16
|
+
defaultPort: 7991,
|
|
17
|
+
fallbackPortRange: {
|
|
18
|
+
min: 8000,
|
|
19
|
+
max: 9000,
|
|
20
|
+
},
|
|
21
|
+
host: 'localhost',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Git configuration
|
|
25
|
+
this.git = {
|
|
26
|
+
timeout: 30000, // 30 seconds
|
|
27
|
+
maxCommitsPerBatch: 100,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// CORS configuration
|
|
31
|
+
this.cors = {
|
|
32
|
+
origin: '*',
|
|
33
|
+
methods: ['GET', 'POST'],
|
|
34
|
+
allowedHeaders: ['Content-Type'],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Logging configuration
|
|
38
|
+
this.logging = {
|
|
39
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
40
|
+
file: {
|
|
41
|
+
enabled: false,
|
|
42
|
+
path: 'logs/app.log',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Repository configuration (set by CLI)
|
|
47
|
+
this.repository = {
|
|
48
|
+
path: null, // Will be set by CLI
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
AppConfig.instance = this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Set the repository path
|
|
56
|
+
* @param {string} path - Full path to the git repository
|
|
57
|
+
*/
|
|
58
|
+
setRepoPath(path) {
|
|
59
|
+
this.repository.path = path;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the repository path
|
|
64
|
+
* @returns {string|null} Repository path
|
|
65
|
+
*/
|
|
66
|
+
getRepoPath() {
|
|
67
|
+
return this.repository.path;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get server configuration
|
|
72
|
+
* @returns {object} Server configuration
|
|
73
|
+
*/
|
|
74
|
+
getServerConfig() {
|
|
75
|
+
return { ...this.server };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get git configuration
|
|
80
|
+
* @returns {object} Git configuration
|
|
81
|
+
*/
|
|
82
|
+
getGitConfig() {
|
|
83
|
+
return { ...this.git };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get CORS configuration
|
|
88
|
+
* @returns {object} CORS configuration
|
|
89
|
+
*/
|
|
90
|
+
getCorsConfig() {
|
|
91
|
+
return { ...this.cors };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get logging configuration
|
|
96
|
+
* @returns {object} Logging configuration
|
|
97
|
+
*/
|
|
98
|
+
getLoggingConfig() {
|
|
99
|
+
return { ...this.logging };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get singleton instance
|
|
104
|
+
* @returns {AppConfig} AppConfig instance
|
|
105
|
+
*/
|
|
106
|
+
static getInstance() {
|
|
107
|
+
if (!AppConfig.instance) {
|
|
108
|
+
AppConfig.instance = new AppConfig();
|
|
109
|
+
}
|
|
110
|
+
return AppConfig.instance;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Export singleton instance
|
|
115
|
+
export default AppConfig.getInstance();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository Configuration Manager
|
|
3
|
+
* Manages saved repository paths and aliases
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
|
|
10
|
+
class RepoManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.configDir = path.join(os.homedir(), '.e-pick');
|
|
13
|
+
this.configFile = path.join(this.configDir, 'repos.json');
|
|
14
|
+
this.ensureConfigExists();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure config directory and file exist
|
|
19
|
+
*/
|
|
20
|
+
ensureConfigExists() {
|
|
21
|
+
try {
|
|
22
|
+
if (!fs.existsSync(this.configDir)) {
|
|
23
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(this.configFile)) {
|
|
27
|
+
fs.writeFileSync(this.configFile, JSON.stringify({}, null, 2));
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`Failed to create config directory: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load all saved repositories
|
|
36
|
+
* @returns {object} Map of alias → path
|
|
37
|
+
*/
|
|
38
|
+
loadRepos() {
|
|
39
|
+
try {
|
|
40
|
+
const data = fs.readFileSync(this.configFile, 'utf8');
|
|
41
|
+
return JSON.parse(data);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Save repositories to config file
|
|
49
|
+
* @param {object} repos - Map of alias → path
|
|
50
|
+
*/
|
|
51
|
+
saveRepos(repos) {
|
|
52
|
+
try {
|
|
53
|
+
fs.writeFileSync(this.configFile, JSON.stringify(repos, null, 2));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(`Failed to save repos: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Save a repository with an alias
|
|
61
|
+
* @param {string} alias - Repository alias
|
|
62
|
+
* @param {string} repoPath - Absolute path to repository
|
|
63
|
+
* @returns {boolean} Success
|
|
64
|
+
*/
|
|
65
|
+
saveRepo(alias, repoPath) {
|
|
66
|
+
const repos = this.loadRepos();
|
|
67
|
+
const absolutePath = path.resolve(repoPath);
|
|
68
|
+
|
|
69
|
+
repos[alias] = absolutePath;
|
|
70
|
+
this.saveRepos(repos);
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get repository path by alias
|
|
77
|
+
* @param {string} alias - Repository alias
|
|
78
|
+
* @returns {string|null} Repository path or null if not found
|
|
79
|
+
*/
|
|
80
|
+
getRepoPath(alias) {
|
|
81
|
+
const repos = this.loadRepos();
|
|
82
|
+
return repos[alias] || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Remove a saved repository
|
|
87
|
+
* @param {string} alias - Repository alias
|
|
88
|
+
* @returns {boolean} Success
|
|
89
|
+
*/
|
|
90
|
+
removeRepo(alias) {
|
|
91
|
+
const repos = this.loadRepos();
|
|
92
|
+
|
|
93
|
+
if (!repos[alias]) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
delete repos[alias];
|
|
98
|
+
this.saveRepos(repos);
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* List all saved repositories
|
|
105
|
+
* @returns {object} Map of alias → path
|
|
106
|
+
*/
|
|
107
|
+
listRepos() {
|
|
108
|
+
return this.loadRepos();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if a repository alias exists
|
|
113
|
+
* @param {string} alias - Repository alias
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
hasRepo(alias) {
|
|
117
|
+
const repos = this.loadRepos();
|
|
118
|
+
return alias in repos;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get repository name from path
|
|
123
|
+
* @param {string} repoPath - Repository path
|
|
124
|
+
* @returns {string} Repository name (folder name)
|
|
125
|
+
*/
|
|
126
|
+
getRepoNameFromPath(repoPath) {
|
|
127
|
+
return path.basename(repoPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default new RepoManager();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit Controller
|
|
3
|
+
*
|
|
4
|
+
* Handles API requests for commit validation and cherry-pick operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import appConfig from '../config/app.config.js';
|
|
8
|
+
import ValidationService from '../services/validation.service.js';
|
|
9
|
+
import GitService from '../services/git.service.js';
|
|
10
|
+
import CherryPickCommand from '../commands/cherry-pick.command.js';
|
|
11
|
+
import { asyncHandler } from '../utils/error-handler.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* POST /api/validate
|
|
15
|
+
* Validate array of commit hashes
|
|
16
|
+
*/
|
|
17
|
+
export const validateCommits = asyncHandler(async (req, res) => {
|
|
18
|
+
const { commits } = req.body;
|
|
19
|
+
const repoPath = appConfig.getRepoPath();
|
|
20
|
+
|
|
21
|
+
const validationService = new ValidationService(repoPath);
|
|
22
|
+
const result = await validationService.validateCommits(commits);
|
|
23
|
+
|
|
24
|
+
res.json({
|
|
25
|
+
success: true,
|
|
26
|
+
...result,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* POST /api/generate-command
|
|
32
|
+
* Generate cherry-pick command from validated commits
|
|
33
|
+
*/
|
|
34
|
+
export const generateCommand = asyncHandler(async (req, res) => {
|
|
35
|
+
const { commits, ignoredCommits = [], options = {} } = req.body;
|
|
36
|
+
const repoPath = appConfig.getRepoPath();
|
|
37
|
+
|
|
38
|
+
// Filter out ignored commits
|
|
39
|
+
const commitsToProcess = commits.filter(
|
|
40
|
+
(commit) => !ignoredCommits.includes(commit),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (commitsToProcess.length === 0) {
|
|
44
|
+
return res.status(400).json({
|
|
45
|
+
success: false,
|
|
46
|
+
error: 'ValidationError',
|
|
47
|
+
message: 'No commits to cherry-pick after filtering ignored commits',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const gitService = new GitService(repoPath);
|
|
52
|
+
const command = new CherryPickCommand(gitService, commitsToProcess, options);
|
|
53
|
+
const result = command.execute();
|
|
54
|
+
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
return res.status(500).json({
|
|
57
|
+
success: false,
|
|
58
|
+
error: 'GitError',
|
|
59
|
+
message: result.error,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
res.json({
|
|
64
|
+
success: true,
|
|
65
|
+
...result,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* GET /api/repo-info
|
|
71
|
+
* Get current repository information
|
|
72
|
+
*/
|
|
73
|
+
export const getRepoInfo = asyncHandler(async (req, res) => {
|
|
74
|
+
const repoPath = appConfig.getRepoPath();
|
|
75
|
+
const gitService = new GitService(repoPath);
|
|
76
|
+
|
|
77
|
+
const repoInfo = gitService.getRepoInfo();
|
|
78
|
+
|
|
79
|
+
res.json({
|
|
80
|
+
success: true,
|
|
81
|
+
repo: repoInfo,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* GET /api/check-commit/:commitHash
|
|
87
|
+
* Check if a commit exists in the repository
|
|
88
|
+
*/
|
|
89
|
+
export const checkCommit = asyncHandler(async (req, res) => {
|
|
90
|
+
const { commitHash } = req.params;
|
|
91
|
+
|
|
92
|
+
const repoPath = appConfig.getRepoPath();
|
|
93
|
+
const gitService = new GitService(repoPath);
|
|
94
|
+
|
|
95
|
+
const exists = gitService.validateCommit(commitHash);
|
|
96
|
+
|
|
97
|
+
res.json({
|
|
98
|
+
success: true,
|
|
99
|
+
exists,
|
|
100
|
+
commit: commitHash,
|
|
101
|
+
});
|
|
102
|
+
});
|