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,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles all UI updates and interactions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class UIManager {
|
|
8
|
+
constructor(apiFacade = null) {
|
|
9
|
+
this.api = apiFacade || new APIFacade();
|
|
10
|
+
this.elements = {};
|
|
11
|
+
this.currentStep = 1;
|
|
12
|
+
this.completedSteps = new Set();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize UI elements
|
|
17
|
+
*/
|
|
18
|
+
init() {
|
|
19
|
+
this.elements = {
|
|
20
|
+
fileInput: document.getElementById('file-input'),
|
|
21
|
+
sheetSelector: document.getElementById('sheet-selector'),
|
|
22
|
+
sheetSearch: document.getElementById('sheet-search'),
|
|
23
|
+
sheetCheckboxes: document.getElementById('sheet-checkboxes'),
|
|
24
|
+
loadSheetsBtn: document.getElementById('load-sheets-btn'),
|
|
25
|
+
dataPreview: document.getElementById('data-preview'),
|
|
26
|
+
columnSelectors: document.getElementById('column-selectors'),
|
|
27
|
+
commitColumnSelect: document.getElementById('commit-column'),
|
|
28
|
+
repoColumnSelect: document.getElementById('repo-column'),
|
|
29
|
+
validateBtn: document.getElementById('validate-btn'),
|
|
30
|
+
validationResults: document.getElementById('validation-results'),
|
|
31
|
+
validationDetails: document.getElementById('validation-details'),
|
|
32
|
+
validCommitsFilterStats: document.getElementById('valid-commits-filter-stats'),
|
|
33
|
+
validCommitsList: document.getElementById('valid-commits-list'),
|
|
34
|
+
invalidCommitsList: document.getElementById('invalid-commits-list'),
|
|
35
|
+
commandOutput: document.getElementById('command-output'),
|
|
36
|
+
commandText: document.getElementById('command-text'),
|
|
37
|
+
copyBtn: document.getElementById('copy-btn'),
|
|
38
|
+
viewFilesBtn: document.getElementById('view-files-btn'),
|
|
39
|
+
generateBtn: document.getElementById('generate-btn'),
|
|
40
|
+
filesChanged: document.getElementById('files-changed'),
|
|
41
|
+
filesList: document.getElementById('files-list'),
|
|
42
|
+
compareBranch: document.getElementById('compare-branch'),
|
|
43
|
+
loadingSpinner: document.getElementById('loading'),
|
|
44
|
+
repoInfo: document.getElementById('repo-info'),
|
|
45
|
+
stepper: document.getElementById('stepper')
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.changedFilesData = null;
|
|
49
|
+
|
|
50
|
+
this.setupStepperNavigation();
|
|
51
|
+
this.setupSheetSearch();
|
|
52
|
+
this.showStep(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Setup sheet search functionality
|
|
57
|
+
*/
|
|
58
|
+
setupSheetSearch() {
|
|
59
|
+
if (this.elements.sheetSearch) {
|
|
60
|
+
this.elements.sheetSearch.addEventListener('input', (e) => {
|
|
61
|
+
this.filterSheets(e.target.value);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Filter sheet checkboxes based on search term
|
|
68
|
+
* @param {string} searchTerm - Search term
|
|
69
|
+
*/
|
|
70
|
+
filterSheets(searchTerm) {
|
|
71
|
+
const labels = this.elements.sheetCheckboxes.querySelectorAll('.sheet-checkbox-label');
|
|
72
|
+
const term = searchTerm.toLowerCase().trim();
|
|
73
|
+
|
|
74
|
+
labels.forEach(label => {
|
|
75
|
+
const sheetName = label.querySelector('span').textContent.toLowerCase();
|
|
76
|
+
if (sheetName.includes(term)) {
|
|
77
|
+
label.style.display = 'flex';
|
|
78
|
+
} else {
|
|
79
|
+
label.style.display = 'none';
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Setup stepper click handlers
|
|
86
|
+
*/
|
|
87
|
+
setupStepperNavigation() {
|
|
88
|
+
const stepCircles = this.elements.stepper.querySelectorAll('.step-circle');
|
|
89
|
+
stepCircles.forEach(circle => {
|
|
90
|
+
circle.addEventListener('click', (e) => {
|
|
91
|
+
const step = parseInt(e.target.getAttribute('data-step'));
|
|
92
|
+
// Only allow navigation to completed steps
|
|
93
|
+
if (this.completedSteps.has(step) || step < this.currentStep) {
|
|
94
|
+
this.goToStep(step);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Navigate to a specific step
|
|
102
|
+
* @param {number} step - Step number to navigate to
|
|
103
|
+
*/
|
|
104
|
+
goToStep(step) {
|
|
105
|
+
if (step < 1 || step > 6) return;
|
|
106
|
+
this.currentStep = step;
|
|
107
|
+
this.showStep(step);
|
|
108
|
+
this.updateStepperUI();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Show only content for the current step
|
|
113
|
+
* @param {number} step - Step number to show
|
|
114
|
+
*/
|
|
115
|
+
showStep(step) {
|
|
116
|
+
// Hide all step contents
|
|
117
|
+
const allStepContents = document.querySelectorAll('[data-step-content]');
|
|
118
|
+
allStepContents.forEach(section => {
|
|
119
|
+
section.style.display = 'none';
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Show only current step content
|
|
123
|
+
const currentStepContents = document.querySelectorAll(`[data-step-content="${step}"]`);
|
|
124
|
+
currentStepContents.forEach(section => {
|
|
125
|
+
// Only show if it's not the sheet selector or it has content
|
|
126
|
+
if (section.id !== 'sheet-selector' || section.querySelector('select')?.options.length > 0) {
|
|
127
|
+
section.style.display = 'block';
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Advance to the next step
|
|
134
|
+
* @param {number} nextStep - Next step number
|
|
135
|
+
*/
|
|
136
|
+
advanceToStep(nextStep) {
|
|
137
|
+
// Mark current step as completed
|
|
138
|
+
this.completedSteps.add(this.currentStep);
|
|
139
|
+
|
|
140
|
+
// Move to next step
|
|
141
|
+
this.currentStep = nextStep;
|
|
142
|
+
this.showStep(nextStep);
|
|
143
|
+
this.updateStepperUI();
|
|
144
|
+
|
|
145
|
+
// Scroll stepper into view
|
|
146
|
+
this.elements.stepper.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Update stepper UI to reflect current state
|
|
151
|
+
*/
|
|
152
|
+
updateStepperUI() {
|
|
153
|
+
const stepperItems = this.elements.stepper.querySelectorAll('.stepper-item');
|
|
154
|
+
|
|
155
|
+
stepperItems.forEach((item, index) => {
|
|
156
|
+
const circle = item.querySelector('.step-circle');
|
|
157
|
+
const step = parseInt(circle.getAttribute('data-step'));
|
|
158
|
+
|
|
159
|
+
// Remove all classes
|
|
160
|
+
circle.classList.remove('active', 'completed');
|
|
161
|
+
item.classList.remove('active', 'completed');
|
|
162
|
+
|
|
163
|
+
// Add appropriate class to both item and circle
|
|
164
|
+
if (step === this.currentStep) {
|
|
165
|
+
circle.classList.add('active');
|
|
166
|
+
item.classList.add('active');
|
|
167
|
+
} else if (this.completedSteps.has(step) || step < this.currentStep) {
|
|
168
|
+
circle.classList.add('completed');
|
|
169
|
+
item.classList.add('completed');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Show loading state
|
|
176
|
+
* @param {string} message - Loading message
|
|
177
|
+
*/
|
|
178
|
+
showLoading(message = 'Processing...') {
|
|
179
|
+
this.elements.loadingSpinner.textContent = message;
|
|
180
|
+
this.elements.loadingSpinner.style.display = 'block';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Hide loading state
|
|
185
|
+
*/
|
|
186
|
+
hideLoading() {
|
|
187
|
+
this.elements.loadingSpinner.style.display = 'none';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Show toast notification
|
|
192
|
+
* @param {string} message - Message to display
|
|
193
|
+
* @param {string} type - Type of toast (success, error, info)
|
|
194
|
+
*/
|
|
195
|
+
showToast(message, type = 'info') {
|
|
196
|
+
const toast = document.createElement('div');
|
|
197
|
+
toast.className = `toast toast-${type}`;
|
|
198
|
+
toast.textContent = message;
|
|
199
|
+
|
|
200
|
+
document.body.appendChild(toast);
|
|
201
|
+
|
|
202
|
+
setTimeout(() => toast.classList.add('show'), 100);
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
toast.classList.remove('show');
|
|
205
|
+
setTimeout(() => toast.remove(), 300);
|
|
206
|
+
}, 3000);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Show sheet selector for Excel files
|
|
211
|
+
* @param {array} sheets - Array of sheet names
|
|
212
|
+
*/
|
|
213
|
+
showSheetSelector(sheets) {
|
|
214
|
+
const checkboxes = sheets.map((sheet, index) => `
|
|
215
|
+
<label class="sheet-checkbox-label">
|
|
216
|
+
<input type="checkbox" name="sheet" value="${sheet}" ${index === 0 ? 'checked' : ''} onchange="window.app.ui.updateLoadSheetsButton()">
|
|
217
|
+
<span>${sheet}</span>
|
|
218
|
+
</label>
|
|
219
|
+
`).join('');
|
|
220
|
+
|
|
221
|
+
this.elements.sheetCheckboxes.innerHTML = checkboxes;
|
|
222
|
+
this.elements.sheetSelector.style.display = 'block';
|
|
223
|
+
this.elements.loadSheetsBtn.disabled = false;
|
|
224
|
+
|
|
225
|
+
// Clear search input
|
|
226
|
+
if (this.elements.sheetSearch) {
|
|
227
|
+
this.elements.sheetSearch.value = '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Auto-scroll to sheet selector
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
this.elements.sheetSelector.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
233
|
+
}, 100);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Update load sheets button state
|
|
238
|
+
*/
|
|
239
|
+
updateLoadSheetsButton() {
|
|
240
|
+
const checkedBoxes = this.elements.sheetCheckboxes.querySelectorAll('input[type="checkbox"]:checked');
|
|
241
|
+
this.elements.loadSheetsBtn.disabled = checkedBoxes.length === 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get selected sheet names
|
|
246
|
+
* @returns {array} Array of selected sheet names
|
|
247
|
+
*/
|
|
248
|
+
getSelectedSheets() {
|
|
249
|
+
const checkedBoxes = this.elements.sheetCheckboxes.querySelectorAll('input[type="checkbox"]:checked');
|
|
250
|
+
return Array.from(checkedBoxes).map(cb => cb.value);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Hide sheet selector
|
|
255
|
+
*/
|
|
256
|
+
hideSheetSelector() {
|
|
257
|
+
this.elements.sheetSelector.style.display = 'none';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Render data preview table
|
|
262
|
+
* @param {object} parsedData - Parsed file data
|
|
263
|
+
*/
|
|
264
|
+
renderDataPreview(parsedData) {
|
|
265
|
+
const { columns, rows, currentSheet } = parsedData;
|
|
266
|
+
|
|
267
|
+
// Show only first 10 rows
|
|
268
|
+
const previewRows = rows.slice(0, 10);
|
|
269
|
+
|
|
270
|
+
let html = `
|
|
271
|
+
<div class="preview-header">
|
|
272
|
+
<h3>Data Preview${currentSheet ? ` - Sheet: ${currentSheet}` : ''}</h3>
|
|
273
|
+
<p class="info-text">${rows.length} rows total, showing first 10</p>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="table-container" tabindex="0" role="region" aria-label="Data preview table">
|
|
276
|
+
<table>
|
|
277
|
+
<thead>
|
|
278
|
+
<tr>
|
|
279
|
+
${columns.map(col => `<th>${col.name}</th>`).join('')}
|
|
280
|
+
</tr>
|
|
281
|
+
</thead>
|
|
282
|
+
<tbody>
|
|
283
|
+
${previewRows.map(row => `
|
|
284
|
+
<tr>
|
|
285
|
+
${row.map(cell => `<td>${cell || ''}</td>`).join('')}
|
|
286
|
+
</tr>
|
|
287
|
+
`).join('')}
|
|
288
|
+
</tbody>
|
|
289
|
+
</table>
|
|
290
|
+
</div>
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
this.elements.dataPreview.innerHTML = html;
|
|
294
|
+
|
|
295
|
+
// Populate column selectors
|
|
296
|
+
this.populateColumnSelectors(columns);
|
|
297
|
+
|
|
298
|
+
// Advance to step 2
|
|
299
|
+
this.advanceToStep(2);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Populate column selector dropdowns
|
|
304
|
+
* @param {array} columns - Column definitions
|
|
305
|
+
*/
|
|
306
|
+
populateColumnSelectors(columns) {
|
|
307
|
+
const commitOptions = columns.map(col =>
|
|
308
|
+
`<option value="${col.index}">${col.name}</option>`
|
|
309
|
+
).join('');
|
|
310
|
+
|
|
311
|
+
const repoOptions = columns.map(col =>
|
|
312
|
+
`<option value="${col.index}">${col.name}</option>`
|
|
313
|
+
).join('');
|
|
314
|
+
|
|
315
|
+
this.elements.commitColumnSelect.innerHTML = commitOptions;
|
|
316
|
+
this.elements.repoColumnSelect.innerHTML = repoOptions;
|
|
317
|
+
|
|
318
|
+
// Try to auto-select commit column (look for "commit" in name)
|
|
319
|
+
const commitCol = columns.find(col =>
|
|
320
|
+
col && col.name && typeof col.name === 'string' &&
|
|
321
|
+
col.name.toLowerCase().includes('commit')
|
|
322
|
+
);
|
|
323
|
+
if (commitCol) {
|
|
324
|
+
this.elements.commitColumnSelect.value = commitCol.index;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Try to auto-select repo column
|
|
328
|
+
const repoCol = columns.find(col =>
|
|
329
|
+
col && col.name && typeof col.name === 'string' &&
|
|
330
|
+
col.name.toLowerCase().includes('repo')
|
|
331
|
+
);
|
|
332
|
+
if (repoCol) {
|
|
333
|
+
this.elements.repoColumnSelect.value = repoCol.index;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this.elements.validateBtn.disabled = false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Render validation results
|
|
341
|
+
* @param {object} results - Validation results
|
|
342
|
+
* @param {function} onToggleIgnore - Callback for ignore toggle
|
|
343
|
+
*/
|
|
344
|
+
renderValidationResults(results, onToggleIgnore) {
|
|
345
|
+
const { results: commits, summary, filterStats } = results;
|
|
346
|
+
|
|
347
|
+
const validCommits = commits.filter(c => c.isValid);
|
|
348
|
+
const invalidCommits = commits.filter(c => !c.isValid);
|
|
349
|
+
|
|
350
|
+
// Add filter statistics if available
|
|
351
|
+
let filterStatsHtml = '';
|
|
352
|
+
if (filterStats) {
|
|
353
|
+
const matchMethod = filterStats.matchedByCommit
|
|
354
|
+
? `<p style="margin-bottom: 0.25rem;"><strong>Match Method:</strong> <span class="success-text">Validated by commit existence</span></p>`
|
|
355
|
+
: '';
|
|
356
|
+
|
|
357
|
+
const repoInfo = filterStats.matchedRepo
|
|
358
|
+
? `<p style="margin-bottom: 0.25rem;"><strong>Matched Repository:</strong> ${filterStats.matchedRepo}</p>`
|
|
359
|
+
: '';
|
|
360
|
+
|
|
361
|
+
filterStatsHtml = `
|
|
362
|
+
<div class="filter-stats ${filterStats.filtered > 0 ? 'warning-box' : 'success-box'}" style="margin-bottom: 1rem;">
|
|
363
|
+
<h3 style="margin-bottom: 0.5rem;">📊 Repository Filter Results</h3>
|
|
364
|
+
${repoInfo}
|
|
365
|
+
${matchMethod}
|
|
366
|
+
<p style="margin-bottom: 0.25rem;"><strong>Total Commits in File:</strong> ${filterStats.total}</p>
|
|
367
|
+
<p style="margin-bottom: 0.25rem;"><strong>Commits for This Repo:</strong> <span class="success-text">${filterStats.matched}</span></p>
|
|
368
|
+
${filterStats.filtered > 0 ? `
|
|
369
|
+
<p style="margin-bottom: 0;" class="warning-text"><strong>Filtered Out (Other Repos):</strong> ${filterStats.filtered}</p>
|
|
370
|
+
<p class="muted-text" style="margin-top: 0.5rem;">
|
|
371
|
+
💡 ${filterStats.filtered} commit(s) from other repositories were automatically excluded
|
|
372
|
+
</p>
|
|
373
|
+
` : ''}
|
|
374
|
+
${filterStats.availableRepos && filterStats.availableRepos.length > 1 ? `
|
|
375
|
+
<p class="muted-text" style="margin-top: 0.5rem;">
|
|
376
|
+
Repositories in file: ${filterStats.availableRepos.join(', ')}
|
|
377
|
+
</p>
|
|
378
|
+
` : ''}
|
|
379
|
+
${filterStats.warning ? `
|
|
380
|
+
<p class="warning-text muted-text" style="margin-top: 0.5rem;">
|
|
381
|
+
⚠️ ${filterStats.warning}
|
|
382
|
+
</p>
|
|
383
|
+
` : ''}
|
|
384
|
+
</div>
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Render invalid commits with note (SHOW FIRST)
|
|
389
|
+
const invalidNote = filterStats && filterStats.filtered > 0
|
|
390
|
+
? `<p class="muted-text" style="margin-bottom: 1rem;">
|
|
391
|
+
<strong>Note:</strong> Showing only invalid commits from the matched repository.
|
|
392
|
+
Commits from other repositories (${filterStats.filtered}) were filtered out automatically.
|
|
393
|
+
</p>`
|
|
394
|
+
: '';
|
|
395
|
+
|
|
396
|
+
this.elements.invalidCommitsList.innerHTML = invalidNote + (invalidCommits.length > 0
|
|
397
|
+
? invalidCommits.map(c => this._renderInvalidCommit(c, onToggleIgnore)).join('')
|
|
398
|
+
: '<p class="empty">No invalid commits in this repository</p>');
|
|
399
|
+
|
|
400
|
+
// Render filter stats (outside scroll)
|
|
401
|
+
this.elements.validCommitsFilterStats.innerHTML = filterStatsHtml;
|
|
402
|
+
|
|
403
|
+
// Render valid commits (inside scroll)
|
|
404
|
+
this.elements.validCommitsList.innerHTML = (validCommits.length > 0
|
|
405
|
+
? validCommits.map(c => this._renderValidCommit(c)).join('')
|
|
406
|
+
: '<p class="empty">No valid commits found</p>');
|
|
407
|
+
|
|
408
|
+
// Show validation details section
|
|
409
|
+
this.elements.validationDetails.style.display = 'block';
|
|
410
|
+
|
|
411
|
+
// Enable generate button if there are valid commits (but keep it hidden)
|
|
412
|
+
this.elements.generateBtn.disabled = validCommits.length === 0;
|
|
413
|
+
|
|
414
|
+
// Advance to step 4
|
|
415
|
+
this.advanceToStep(4);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Render a valid commit
|
|
420
|
+
* @param {object} commit - Commit data
|
|
421
|
+
* @returns {string} HTML string
|
|
422
|
+
* @private
|
|
423
|
+
*/
|
|
424
|
+
_renderValidCommit(commit) {
|
|
425
|
+
const { commitInfo } = commit;
|
|
426
|
+
return `
|
|
427
|
+
<div class="commit-item commit-valid">
|
|
428
|
+
<div class="commit-hash">
|
|
429
|
+
<span class="badge badge-success">✓</span>
|
|
430
|
+
<code>${commitInfo.shortHash}</code>
|
|
431
|
+
</div>
|
|
432
|
+
<div class="commit-details">
|
|
433
|
+
<div class="commit-message">${commitInfo.message}</div>
|
|
434
|
+
<div class="commit-meta">
|
|
435
|
+
${commitInfo.author} • ${new Date(commitInfo.date).toLocaleString()}
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Render an invalid commit
|
|
444
|
+
* @param {object} commit - Commit data
|
|
445
|
+
* @param {function} onToggleIgnore - Callback for ignore toggle
|
|
446
|
+
* @returns {string} HTML string
|
|
447
|
+
* @private
|
|
448
|
+
*/
|
|
449
|
+
_renderInvalidCommit(commit, onToggleIgnore) {
|
|
450
|
+
const badgeClass = commit.canIgnore ? 'badge-warning' : 'badge-error';
|
|
451
|
+
const badgeIcon = commit.canIgnore ? '⚠' : '✗';
|
|
452
|
+
|
|
453
|
+
return `
|
|
454
|
+
<div class="commit-item commit-invalid" style="padding: 0.75rem;">
|
|
455
|
+
<details>
|
|
456
|
+
<summary style="cursor: pointer; display: flex; align-items: center; gap: 1rem; list-style: none;">
|
|
457
|
+
<span class="badge ${badgeClass}">${badgeIcon}</span>
|
|
458
|
+
<code class="strong-text">${commit.commit.substring(0, 7)}</code>
|
|
459
|
+
<span class="error-text" style="flex: 1;">${commit.error.message}</span>
|
|
460
|
+
<span class="muted-text">▼ View Details</span>
|
|
461
|
+
</summary>
|
|
462
|
+
<div class="error-box" style="margin-top: 1rem;">
|
|
463
|
+
<div class="commit-details">
|
|
464
|
+
<div style="margin-bottom: 0.5rem;">
|
|
465
|
+
<strong class="strong-text">Full Hash:</strong>
|
|
466
|
+
<code class="inline-code" style="display: block; margin-top: 0.25rem;">${commit.commit}</code>
|
|
467
|
+
</div>
|
|
468
|
+
<div style="margin-bottom: 0.5rem;">
|
|
469
|
+
<strong class="strong-text">Error Type:</strong>
|
|
470
|
+
<code class="commit-error-type" style="margin-left: 0.5rem;">${commit.error.type}</code>
|
|
471
|
+
</div>
|
|
472
|
+
<div style="margin-bottom: 0.5rem;">
|
|
473
|
+
<strong class="strong-text">Error Message:</strong>
|
|
474
|
+
<p class="error-text" style="margin-top: 0.25rem;">${commit.error.message}</p>
|
|
475
|
+
</div>
|
|
476
|
+
${commit.canIgnore ? `
|
|
477
|
+
<div class="warning-box" style="margin-top: 1rem;">
|
|
478
|
+
<label class="ignore-checkbox">
|
|
479
|
+
<input type="checkbox" data-commit="${commit.commit}" onchange="window.app.handleIgnoreToggle('${commit.commit}')">
|
|
480
|
+
<span class="warning-text">Ignore this commit (can be safely skipped)</span>
|
|
481
|
+
</label>
|
|
482
|
+
</div>
|
|
483
|
+
` : ''}
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</details>
|
|
487
|
+
</div>
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Display generated command
|
|
493
|
+
* @param {object} commandData - Command data
|
|
494
|
+
*/
|
|
495
|
+
displayCommand(commandData) {
|
|
496
|
+
const { command, commits, warning, changedFiles } = commandData;
|
|
497
|
+
|
|
498
|
+
const fileCount = changedFiles ? changedFiles.length : 0;
|
|
499
|
+
|
|
500
|
+
let html = `
|
|
501
|
+
<div class="command-box">
|
|
502
|
+
<pre><code>${command}</code></pre>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="command-info" style="margin-bottom: 1.5rem; display: flex; gap: 2rem; align-items: center;">
|
|
505
|
+
<p><strong>Commits to cherry-pick:</strong> ${commits.length}</p>
|
|
506
|
+
<p class="success-box" style="padding: 0.5rem 1rem; margin: 0;">
|
|
507
|
+
<strong>Files changed:</strong> <span class="success-text" style="font-size: 1.1rem;">${fileCount}</span>
|
|
508
|
+
</p>
|
|
509
|
+
</div>
|
|
510
|
+
${warning ? `<p class="warning" style="margin-bottom: 1rem;">⚠ ${warning}</p>` : ''}
|
|
511
|
+
|
|
512
|
+
<details open style="margin-top: 1rem;">
|
|
513
|
+
<summary class="details-summary">
|
|
514
|
+
📋 Commit Order Details (${commits.length} commits)
|
|
515
|
+
</summary>
|
|
516
|
+
<div class="details-content" tabindex="0" role="region" aria-label="Commit order details">
|
|
517
|
+
${commits.map((c, i) => `
|
|
518
|
+
<div class="commit-order-item">
|
|
519
|
+
<span class="order-num">${i + 1}</span>
|
|
520
|
+
<code>${c.shortHash}</code>
|
|
521
|
+
<span class="commit-msg">${c.message}</span>
|
|
522
|
+
</div>
|
|
523
|
+
`).join('')}
|
|
524
|
+
</div>
|
|
525
|
+
</details>
|
|
526
|
+
`;
|
|
527
|
+
|
|
528
|
+
this.elements.commandText.innerHTML = html;
|
|
529
|
+
this.elements.copyBtn.disabled = false;
|
|
530
|
+
|
|
531
|
+
// Store command and changed files for later use
|
|
532
|
+
this.currentCommand = command;
|
|
533
|
+
this.changedFilesData = changedFiles || [];
|
|
534
|
+
|
|
535
|
+
// Always enable view files button - displayChangedFiles() will handle empty state
|
|
536
|
+
this.elements.viewFilesBtn.disabled = false;
|
|
537
|
+
|
|
538
|
+
// Show command output section (stay in step 4)
|
|
539
|
+
this.elements.commandOutput.style.display = 'block';
|
|
540
|
+
|
|
541
|
+
// Scroll to command output smoothly
|
|
542
|
+
setTimeout(() => {
|
|
543
|
+
this.elements.commandOutput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
544
|
+
}, 100);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Display changed files with diff commands
|
|
549
|
+
*/
|
|
550
|
+
displayChangedFiles() {
|
|
551
|
+
if (!this.changedFilesData || this.changedFilesData.length === 0) {
|
|
552
|
+
this.elements.filesList.innerHTML = '<p class="empty">No files changed</p>';
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const branch = this.elements.compareBranch.value || 'origin/develop';
|
|
557
|
+
|
|
558
|
+
// Create single command to check only the changed files from commits
|
|
559
|
+
const diffCommand = `git diff ${branch} -- ${this.changedFilesData.join(' ')}`;
|
|
560
|
+
|
|
561
|
+
// Create files list
|
|
562
|
+
const filesHtml = this.changedFilesData.map((file, index) => {
|
|
563
|
+
return `
|
|
564
|
+
<div style="display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0;">
|
|
565
|
+
<span class="badge-number">${index + 1}</span>
|
|
566
|
+
<code class="file-code">${file}</code>
|
|
567
|
+
</div>
|
|
568
|
+
`;
|
|
569
|
+
}).join('');
|
|
570
|
+
|
|
571
|
+
this.elements.filesList.innerHTML = `
|
|
572
|
+
<!-- Command to check all files -->
|
|
573
|
+
<div class="success-box" style="margin-bottom: 1.5rem;">
|
|
574
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
|
575
|
+
<h3 class="success-text" style="margin: 0; font-size: 1.1rem;">📝 Git Diff Command</h3>
|
|
576
|
+
<button
|
|
577
|
+
class="btn btn-primary"
|
|
578
|
+
style="padding: 0.5rem 1rem; font-size: 0.9rem; margin: 0;"
|
|
579
|
+
onclick="navigator.clipboard.writeText('${diffCommand}'); window.app.ui.showToast('Copied!', 'success');"
|
|
580
|
+
>
|
|
581
|
+
📋 Copy Command
|
|
582
|
+
</button>
|
|
583
|
+
</div>
|
|
584
|
+
<p class="muted-text" style="margin-bottom: 1rem;">
|
|
585
|
+
Use this command to see all changes compared to <strong>${branch}</strong> branch:
|
|
586
|
+
</p>
|
|
587
|
+
<div class="code-box">
|
|
588
|
+
<pre style="margin: 0; overflow-x: auto;"><code class="code-text">${diffCommand}</code></pre>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<!-- Files list -->
|
|
593
|
+
<div class="info-box">
|
|
594
|
+
<h3 class="strong-text" style="margin: 0 0 1rem 0; font-size: 1rem;">
|
|
595
|
+
📂 Changed Files (<strong>${this.changedFilesData.length}</strong> files)
|
|
596
|
+
</h3>
|
|
597
|
+
<div class="details-content" style="padding-right: 0.5rem;" tabindex="0" role="region" aria-label="Changed files list">
|
|
598
|
+
${filesHtml}
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
`;
|
|
602
|
+
|
|
603
|
+
// Add event listener for branch input change (only once)
|
|
604
|
+
const branchInput = this.elements.compareBranch;
|
|
605
|
+
const newBranchInput = branchInput.cloneNode(true);
|
|
606
|
+
branchInput.parentNode.replaceChild(newBranchInput, branchInput);
|
|
607
|
+
this.elements.compareBranch = newBranchInput;
|
|
608
|
+
|
|
609
|
+
newBranchInput.addEventListener('input', () => {
|
|
610
|
+
this.displayChangedFiles();
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Advance to step 5 (View Changed Files)
|
|
614
|
+
this.advanceToStep(5);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Copy command to clipboard
|
|
619
|
+
*/
|
|
620
|
+
async copyToClipboard() {
|
|
621
|
+
if (!this.currentCommand) return;
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
await navigator.clipboard.writeText(this.currentCommand);
|
|
625
|
+
this.showToast('Command copied to clipboard!', 'success');
|
|
626
|
+
} catch (error) {
|
|
627
|
+
this.showToast('Failed to copy command', 'error');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Load and display repository info
|
|
633
|
+
*/
|
|
634
|
+
async loadRepoInfo() {
|
|
635
|
+
try {
|
|
636
|
+
const data = await this.api.getRepoInfo();
|
|
637
|
+
|
|
638
|
+
if (data.success) {
|
|
639
|
+
const { repo } = data;
|
|
640
|
+
this.elements.repoInfo.innerHTML = `
|
|
641
|
+
<strong>${repo.repoName}</strong> •
|
|
642
|
+
Branch: ${repo.branch} •
|
|
643
|
+
Last commit: ${repo.lastCommit.hash}
|
|
644
|
+
`;
|
|
645
|
+
}
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.error('Failed to load repo info:', error);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Load and display version from API
|
|
653
|
+
*/
|
|
654
|
+
async loadVersion() {
|
|
655
|
+
try {
|
|
656
|
+
const data = await this.api.getVersion();
|
|
657
|
+
const versionEl = document.getElementById('version-info');
|
|
658
|
+
if (versionEl && data.version) {
|
|
659
|
+
versionEl.textContent = `E-Pick Tool v${data.version}`;
|
|
660
|
+
}
|
|
661
|
+
} catch (error) {
|
|
662
|
+
console.error('Failed to load version:', error);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Export for use in other scripts
|
|
668
|
+
window.UIManager = UIManager;
|