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/public/index.html CHANGED
@@ -3,42 +3,224 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Excel File Processor</title>
7
- <link rel="stylesheet" href="styles.css">
6
+ <title>E-Pick Tool</title>
7
+ <link rel="stylesheet" href="css/styles.css">
8
+
9
+ <!-- External Libraries -->
10
+ <script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
8
12
  </head>
9
13
  <body>
10
14
  <div class="container">
11
- <h1>Excel File Processor</h1>
12
- <input type="file" id="fileInput" accept=".xlsx, .xls">
13
- <div id="loading" style="display: none;">Loading...</div>
14
- <div id="searchContainer" style="display: none;">
15
- <input type="text" id="searchBox" list="sheetSuggestions" placeholder="Search for a sheet...">
16
- <datalist id="sheetSuggestions"></datalist>
17
- </div>
18
- <ul id="selectedSheetList"></ul>
19
- <div id="tableContainer"></div>
20
- <div>
21
- <label for="headerSelector1">Repo Column:</label>
22
- <select id="headerSelector1" disabled></select>
23
- </div>
24
- <div>
25
- <label for="headerSelector2">Commit ID Column:</label>
26
- <select id="headerSelector2" disabled></select>
27
- </div>
28
- <div id="submitContainer">
29
- <button id="submitButton">Submit</button>
30
- </div>
15
+ <header>
16
+ <h1>🍒 E-Pick Tool</h1>
17
+ <p class="subtitle">Cherry-pick git commits from Excel/CSV files</p>
18
+ <div class="repo-info" id="repo-info">Loading repository info...</div>
19
+ </header>
20
+
21
+ <main>
22
+ <!-- Progress Stepper -->
23
+ <div class="stepper" id="stepper">
24
+ <div class="stepper-item active">
25
+ <div class="step-circle active" data-step="1">1</div>
26
+ <div class="step-label">Upload File</div>
27
+ </div>
28
+ <div class="stepper-item">
29
+ <div class="step-circle" data-step="2">2</div>
30
+ <div class="step-label">Select Columns</div>
31
+ </div>
32
+ <div class="stepper-item">
33
+ <div class="step-circle" data-step="3">3</div>
34
+ <div class="step-label">Validate</div>
35
+ </div>
36
+ <div class="stepper-item">
37
+ <div class="step-circle" data-step="4">4</div>
38
+ <div class="step-label">Review & Get Command</div>
39
+ </div>
40
+ <div class="stepper-item">
41
+ <div class="step-circle" data-step="5">5</div>
42
+ <div class="step-label">View Changed Files</div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Step 1: File Upload -->
47
+ <section class="card step-card" data-step-content="1">
48
+ <h2>Step 1: Upload File</h2>
49
+ <p>Select an Excel (.xlsx, .xls) or CSV file containing commit information</p>
50
+ <div class="file-input-container">
51
+ <input type="file" id="file-input" accept=".xlsx,.xls,.csv,.tsv" />
52
+ <label for="file-input" class="file-label">
53
+ Choose File or Drag & Drop
54
+ </label>
55
+ </div>
56
+ </section>
57
+
58
+ <!-- Step 1.5: Sheet Selection (for Excel files) -->
59
+ <section class="card" id="sheet-selector" data-step-content="1" style="display: none;">
60
+ <h2>Select Sheets</h2>
61
+ <p>This Excel file contains multiple sheets. Select one or more sheets to combine:</p>
62
+
63
+ <div style="margin: 1rem 0;">
64
+ <label for="sheet-search" style="display: block; font-weight: 600; color: var(--subtext1); margin-bottom: 0.5rem;">
65
+ Search Sheets:
66
+ </label>
67
+ <input
68
+ type="text"
69
+ id="sheet-search"
70
+ placeholder="Search sheets..."
71
+ aria-label="Search sheets"
72
+ >
73
+ </div>
74
+
75
+ <div id="sheet-checkboxes" role="group" aria-label="Sheet selection" tabindex="0">
76
+ <!-- Checkboxes will be inserted here -->
77
+ </div>
78
+
79
+ <button id="load-sheets-btn" class="btn btn-primary" disabled>
80
+ Load Selected Sheets
81
+ </button>
82
+ </section>
83
+
84
+ <!-- Step 2: Column Selection -->
85
+ <section class="card" id="column-selectors" data-step-content="2" style="display: none;">
86
+ <h2>Step 2: Select Columns</h2>
87
+
88
+ <!-- Action button at top -->
89
+ <div class="action-section">
90
+ <button id="validate-btn" class="btn btn-primary" style="font-size: 1.1rem; padding: 1rem 2rem;" disabled>
91
+ ✓ Validate Commits
92
+ </button>
93
+ <p class="info-text" style="margin-top: 0.5rem;">Select columns below, then validate</p>
94
+ </div>
95
+
96
+ <!-- Column selectors -->
97
+ <div class="selector-group">
98
+ <div class="selector">
99
+ <label for="commit-column">Commit Hash Column:</label>
100
+ <select id="commit-column"></select>
101
+ </div>
102
+ <div class="selector">
103
+ <label for="repo-column">Repository Column (optional):</label>
104
+ <select id="repo-column"></select>
105
+ </div>
106
+ </div>
107
+ </section>
108
+
109
+ <!-- Data Preview -->
110
+ <section class="card" id="data-preview" data-step-content="2" style="display: none;">
111
+ <h3 style="margin-bottom: 1rem;">Data Preview</h3>
112
+ <!-- Preview content will be inserted here -->
113
+ </section>
114
+
115
+ <!-- Step 3: Validation Progress -->
116
+ <section class="card" id="validation-progress" data-step-content="3" style="display: none;">
117
+ <h2>Step 3: Validating Commits</h2>
118
+ <p class="info-text">Please wait while we validate your commits...</p>
119
+ <div style="text-align: center; padding: 2rem;">
120
+ <div style="font-size: 3rem; color: var(--blue);">⏳</div>
121
+ <p style="margin-top: 1rem; color: var(--subtext1);">Checking commits in repository...</p>
122
+ </div>
123
+ </section>
124
+
125
+ <!-- Step 4: Validation Results & Command -->
126
+ <section class="card" id="validation-results" data-step-content="4" style="display: none;">
127
+ <h2>Step 4: Review Results & Get Command</h2>
128
+
129
+ <!-- Command Output (shown first) -->
130
+ <div id="command-output" style="display: none;">
131
+ <h3 style="margin-bottom: 1rem;">📋 Cherry-Pick Command</h3>
132
+
133
+ <!-- Action buttons -->
134
+ <div class="action-section">
135
+ <div style="display: flex; gap: 1rem; flex-wrap: wrap;">
136
+ <button id="copy-btn" class="btn btn-primary" style="font-size: 1.1rem; padding: 1rem 2rem;" disabled>
137
+ 📋 Copy Command
138
+ </button>
139
+ <button id="view-files-btn" class="btn btn-success" style="font-size: 1.1rem; padding: 1rem 2rem;" disabled>
140
+ 📄 View Changed Files
141
+ </button>
142
+ </div>
143
+ <p class="info-text" style="margin-top: 0.5rem;">Copy command to clipboard or click "View Changed Files" to see diff commands for each file</p>
144
+ </div>
145
+
146
+ <!-- Command and details -->
147
+ <div id="command-text"></div>
148
+ </div>
149
+
150
+ <!-- Validation Details (below command) -->
151
+ <div id="validation-details" style="display: none; margin-top: 2rem; padding-top: 2rem; border-top: 2px solid var(--surface0);">
152
+ <h3 style="margin-bottom: 1rem;">📊 Validation Details</h3>
153
+
154
+ <!-- Scrollable details -->
155
+ <div style="max-height: 500px; overflow-y: auto;" tabindex="0" role="region" aria-label="Validation details">
156
+ <div class="results-section">
157
+ <h4>⚠ Invalid Commits</h4>
158
+ <p class="info-text">You can ignore commits that can be safely skipped</p>
159
+ <div id="invalid-commits-list" class="commits-list" tabindex="0" role="region" aria-label="Invalid commits list"></div>
160
+ </div>
161
+
162
+ <div class="results-section">
163
+ <h4>✓ Valid Commits</h4>
164
+ <div id="valid-commits-filter-stats"></div>
165
+ <div id="valid-commits-list" class="commits-list" tabindex="0" role="region" aria-label="Valid commits list"></div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Hidden generate button (for backward compatibility) -->
171
+ <button id="generate-btn" style="display: none;"></button>
172
+ </section>
173
+
174
+ <!-- Step 5: Files Changed -->
175
+ <section class="card" id="files-changed" data-step-content="5" style="display: none;">
176
+ <h2>Step 5: View Changed Files</h2>
177
+ <p class="info-text">Files modified by the selected commits with diff commands</p>
178
+
179
+ <div style="margin-bottom: 1.5rem;">
180
+ <label for="compare-branch" class="input-label">
181
+ Compare Branch:
182
+ </label>
183
+ <input
184
+ type="text"
185
+ id="compare-branch"
186
+ value="origin/develop"
187
+ placeholder="origin/develop"
188
+ class="branch-input"
189
+ >
190
+ <span class="info-text" style="margin-left: 1rem; font-size: 0.9rem;">
191
+ ℹ️ Branch to compare against
192
+ </span>
193
+ </div>
194
+
195
+ <div id="files-list" style="margin-top: 1.5rem;">
196
+ <!-- Files will be inserted here -->
197
+ </div>
198
+ </section>
199
+
200
+ <!-- Loading Spinner -->
201
+ <div id="loading" class="loading" style="display: none;">
202
+ Loading...
203
+ </div>
204
+ </main>
205
+
206
+ <footer>
207
+ <p id="version-info">E-Pick Tool</p>
208
+ </footer>
31
209
  </div>
32
- <!-- Modal for displaying success and error messages -->
33
- <div id="modal" class="modal">
34
- <div class="modal-content">
35
- <span class="close-button" id="closeButton">&times;</span>
36
- <p id="modalMessage"></p>
37
- <button id="copyButton" style="display: none;">Copy Command</button>
38
- </div>
39
- </div>
40
- <!-- Include the xlsx library -->
41
- <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.9/xlsx.full.min.js"></script>
42
- <script src="script.js"></script>
210
+
211
+ <!-- Application Scripts -->
212
+ <script src="js/observable.js"></script>
213
+ <script src="js/api-facade.js"></script>
214
+ <script src="js/parsers/base-parser.js"></script>
215
+ <script src="js/parsers/excel-parser.js"></script>
216
+ <script src="js/parsers/csv-parser.js"></script>
217
+ <script src="js/parsers/parser-factory.js"></script>
218
+ <script src="js/file-parser.js"></script>
219
+ <script src="js/cherry-pick-builder.js"></script>
220
+ <script src="js/stepper-states.js"></script>
221
+ <script src="js/filter-strategy.js"></script>
222
+ <script src="js/commit-validator.js"></script>
223
+ <script src="js/ui-manager.js"></script>
224
+ <script src="js/app.js"></script>
43
225
  </body>
44
- </html>
226
+ </html>
@@ -0,0 +1,113 @@
1
+ /**
2
+ * API Facade (Facade Pattern)
3
+ *
4
+ * Provides a simplified interface for all backend API interactions.
5
+ * Centralizes error handling, request formatting, and response parsing.
6
+ */
7
+
8
+ class APIFacade {
9
+ constructor(baseUrl = '/api') {
10
+ this.baseUrl = baseUrl;
11
+ }
12
+
13
+ /**
14
+ * Validate array of commit hashes
15
+ * @param {string[]} commits - Array of commit hashes
16
+ * @returns {Promise<object>} Validation results
17
+ */
18
+ async validateCommits(commits) {
19
+ return this._post('/validate', { commits });
20
+ }
21
+
22
+ /**
23
+ * Generate cherry-pick command
24
+ * @param {string[]} commits - Valid commit hashes
25
+ * @param {string[]} ignoredCommits - Commits to ignore
26
+ * @param {object} options - Additional options
27
+ * @returns {Promise<object>} Generated command data
28
+ */
29
+ async generateCommand(commits, ignoredCommits = [], options = {}) {
30
+ return this._post('/generate-command', {
31
+ commits,
32
+ ignoredCommits,
33
+ options
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Check if a commit exists in repository
39
+ * @param {string} commitHash - Commit hash to check
40
+ * @returns {Promise<object>} Existence check result
41
+ */
42
+ async checkCommit(commitHash) {
43
+ return this._get(`/check-commit/${commitHash}`);
44
+ }
45
+
46
+ /**
47
+ * Get repository information
48
+ * @returns {Promise<object>} Repository info
49
+ */
50
+ async getRepoInfo() {
51
+ return this._get('/repo-info');
52
+ }
53
+
54
+ /**
55
+ * Get application version
56
+ * @returns {Promise<object>} Version info
57
+ */
58
+ async getVersion() {
59
+ return this._get('/version');
60
+ }
61
+
62
+ /**
63
+ * Perform GET request
64
+ * @param {string} endpoint - API endpoint
65
+ * @returns {Promise<object>} Response data
66
+ * @private
67
+ */
68
+ async _get(endpoint) {
69
+ try {
70
+ const response = await fetch(`${this.baseUrl}${endpoint}`);
71
+
72
+ if (!response.ok) {
73
+ const error = await response.json();
74
+ throw new Error(error.message || `Request failed: ${response.status}`);
75
+ }
76
+
77
+ return await response.json();
78
+ } catch (error) {
79
+ throw new Error(`API GET ${endpoint} failed: ${error.message}`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Perform POST request
85
+ * @param {string} endpoint - API endpoint
86
+ * @param {object} data - Request body data
87
+ * @returns {Promise<object>} Response data
88
+ * @private
89
+ */
90
+ async _post(endpoint, data) {
91
+ try {
92
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json'
96
+ },
97
+ body: JSON.stringify(data)
98
+ });
99
+
100
+ if (!response.ok) {
101
+ const error = await response.json();
102
+ throw new Error(error.message || `Request failed: ${response.status}`);
103
+ }
104
+
105
+ return await response.json();
106
+ } catch (error) {
107
+ throw new Error(`API POST ${endpoint} failed: ${error.message}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ // Export singleton instance
113
+ window.APIFacade = APIFacade;
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Main Application
3
+ *
4
+ * Orchestrates all modules and handles main application flow
5
+ */
6
+
7
+ class App {
8
+ constructor() {
9
+ this.ui = new UIManager();
10
+ this.validator = new CommitValidator();
11
+ this.parsedData = null;
12
+ this.currentParser = null;
13
+ this.currentFile = null;
14
+ }
15
+
16
+ /**
17
+ * Initialize application
18
+ */
19
+ init() {
20
+ this.ui.init();
21
+ this.setupEventListeners();
22
+ this.ui.loadRepoInfo();
23
+ this.ui.loadVersion();
24
+ }
25
+
26
+ /**
27
+ * Setup event listeners
28
+ */
29
+ setupEventListeners() {
30
+ // File input
31
+ this.ui.elements.fileInput.addEventListener('change', (e) => {
32
+ this.handleFileSelect(e.target.files[0]);
33
+ });
34
+
35
+ // Load sheets button
36
+ this.ui.elements.loadSheetsBtn.addEventListener('click', () => {
37
+ this.handleLoadSheets();
38
+ });
39
+
40
+ // Validate button
41
+ this.ui.elements.validateBtn.addEventListener('click', () => {
42
+ this.handleValidate();
43
+ });
44
+
45
+ // Generate command button
46
+ this.ui.elements.generateBtn.addEventListener('click', () => {
47
+ this.handleGenerateCommand();
48
+ });
49
+
50
+ // Copy button
51
+ this.ui.elements.copyBtn.addEventListener('click', () => {
52
+ this.ui.copyToClipboard();
53
+ });
54
+
55
+ // View files button
56
+ this.ui.elements.viewFilesBtn.addEventListener('click', () => {
57
+ this.ui.displayChangedFiles();
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Handle file selection
63
+ * @param {File} file - Selected file
64
+ */
65
+ async handleFileSelect(file) {
66
+ if (!file) return;
67
+
68
+ this.currentFile = file;
69
+ this.ui.showLoading('Parsing file...');
70
+
71
+ try {
72
+ const result = await FileParser.parseFile(file);
73
+
74
+ if (!result.success) {
75
+ throw new Error(result.error);
76
+ }
77
+
78
+ // Check if sheet selection is needed
79
+ if (result.needsSheetSelection) {
80
+ this.currentParser = result.parser;
81
+ this.ui.showSheetSelector(result.sheets);
82
+ this.ui.showToast('Please select a sheet to continue', 'info');
83
+ } else {
84
+ // CSV or single-sheet Excel - parse directly
85
+ this.parsedData = result;
86
+ this.ui.renderDataPreview(result);
87
+ this.ui.showToast(`File parsed successfully! ${result.rows.length} rows found`, 'success');
88
+ }
89
+ } catch (error) {
90
+ this.ui.showToast(error.message, 'error');
91
+ } finally {
92
+ this.ui.hideLoading();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Handle loading selected sheets
98
+ */
99
+ async handleLoadSheets() {
100
+ if (!this.currentParser) return;
101
+
102
+ const selectedSheets = this.ui.getSelectedSheets();
103
+ if (selectedSheets.length === 0) {
104
+ this.ui.showToast('Please select at least one sheet', 'error');
105
+ return;
106
+ }
107
+
108
+ this.ui.showLoading(`Loading ${selectedSheets.length} sheet(s)...`);
109
+
110
+ try {
111
+ // Parse all selected sheets
112
+ const sheetsData = await Promise.all(
113
+ selectedSheets.map(sheetName => FileParser.parseSheet(this.currentParser, sheetName))
114
+ );
115
+
116
+ // Check for errors
117
+ const errors = sheetsData.filter(data => !data.success);
118
+ if (errors.length > 0) {
119
+ throw new Error(errors[0].error);
120
+ }
121
+
122
+ // Merge all sheets data
123
+ let mergedData;
124
+ if (sheetsData.length === 1) {
125
+ mergedData = sheetsData[0];
126
+ } else {
127
+ // Combine rows from all sheets
128
+ const allRows = [];
129
+ const firstSheet = sheetsData[0];
130
+
131
+ sheetsData.forEach(sheetData => {
132
+ allRows.push(...sheetData.rows);
133
+ });
134
+
135
+ mergedData = {
136
+ success: true,
137
+ columns: firstSheet.columns,
138
+ rows: allRows,
139
+ currentSheet: selectedSheets.join(', ')
140
+ };
141
+ }
142
+
143
+ this.parsedData = mergedData;
144
+ this.ui.hideSheetSelector();
145
+ this.ui.renderDataPreview(mergedData);
146
+ this.ui.showToast(
147
+ `${selectedSheets.length} sheet(s) loaded! ${mergedData.rows.length} total rows`,
148
+ 'success'
149
+ );
150
+ } catch (error) {
151
+ this.ui.showToast(error.message, 'error');
152
+ } finally {
153
+ this.ui.hideLoading();
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Handle validate button click
159
+ */
160
+ async handleValidate() {
161
+ if (!this.parsedData) {
162
+ this.ui.showToast('Please select a file first', 'error');
163
+ return;
164
+ }
165
+
166
+ const commitColumnIndex = parseInt(this.ui.elements.commitColumnSelect.value);
167
+ const repoColumnIndex = parseInt(this.ui.elements.repoColumnSelect.value);
168
+
169
+ // Advance to step 3 (validation in progress)
170
+ this.ui.advanceToStep(3);
171
+
172
+ this.ui.showLoading('Validating commits...');
173
+
174
+ try {
175
+ // Extract commits from data with repo column
176
+ const { commits, repoData } = this.validator.extractCommits(
177
+ this.parsedData.rows,
178
+ commitColumnIndex,
179
+ !isNaN(repoColumnIndex) ? repoColumnIndex : null
180
+ );
181
+
182
+ if (commits.length === 0) {
183
+ throw new Error('No commits found in selected column');
184
+ }
185
+
186
+ // Filter commits by current repository (validates commits exist)
187
+ const filterResult = await this.validator.filterByRepo(
188
+ commits,
189
+ repoData
190
+ );
191
+
192
+ // Show filter statistics if any commits were filtered
193
+ if (filterResult.stats.filtered > 0) {
194
+ const message = filterResult.stats.matchedByCommit
195
+ ? `Matched repository: ${filterResult.stats.matchedRepo}. Filtered out ${filterResult.stats.filtered} commit(s) from other repositories.`
196
+ : `Filtered out ${filterResult.stats.filtered} commit(s) from other repositories`;
197
+ this.ui.showToast(message, 'info');
198
+ } else if (filterResult.stats.warning) {
199
+ this.ui.showToast(filterResult.stats.warning, 'info');
200
+ } else if (filterResult.stats.matchedByCommit) {
201
+ this.ui.showToast(
202
+ `Repository matched by commit validation: ${filterResult.stats.matchedRepo}`,
203
+ 'success'
204
+ );
205
+ }
206
+
207
+ // Validate filtered commits
208
+ const results = await this.validator.validateCommits(
209
+ filterResult.filtered,
210
+ filterResult.stats
211
+ );
212
+
213
+ // Render results
214
+ this.ui.renderValidationResults(results, (commit) => {
215
+ this.handleIgnoreToggle(commit);
216
+ });
217
+
218
+ // Build success message
219
+ let message = `Validation complete: ${results.summary.valid} valid, ${results.summary.invalid} invalid`;
220
+ if (filterResult.stats.filtered > 0) {
221
+ message += ` (${filterResult.stats.filtered} filtered)`;
222
+ }
223
+
224
+ this.ui.showToast(
225
+ message,
226
+ results.summary.invalid === 0 ? 'success' : 'info'
227
+ );
228
+
229
+ // Auto-generate command if there are valid commits
230
+ if (results.summary.valid > 0) {
231
+ await this.handleGenerateCommand();
232
+ }
233
+ } catch (error) {
234
+ this.ui.showToast(error.message, 'error');
235
+ } finally {
236
+ this.ui.hideLoading();
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Handle ignore checkbox toggle
242
+ * @param {string} commit - Commit hash
243
+ */
244
+ handleIgnoreToggle(commit) {
245
+ this.validator.toggleIgnore(commit);
246
+
247
+ const ignoredCount = this.validator.getIgnoredCommits().length;
248
+ if (ignoredCount > 0) {
249
+ this.ui.showToast(`${ignoredCount} commit(s) will be ignored`, 'info');
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Handle generate command button click
255
+ */
256
+ async handleGenerateCommand() {
257
+ if (!this.validator.validationResults) {
258
+ this.ui.showToast('Please validate commits first', 'error');
259
+ return;
260
+ }
261
+
262
+ this.ui.showLoading('Generating command...');
263
+
264
+ try {
265
+ const result = await this.validator.generateCommand();
266
+
267
+ if (!result.success) {
268
+ throw new Error(result.error);
269
+ }
270
+
271
+ this.ui.displayCommand(result);
272
+ this.ui.showToast('Command generated successfully!', 'success');
273
+ } catch (error) {
274
+ this.ui.showToast(error.message, 'error');
275
+ } finally {
276
+ this.ui.hideLoading();
277
+ }
278
+ }
279
+ }
280
+
281
+ // Initialize app when DOM is ready
282
+ document.addEventListener('DOMContentLoaded', () => {
283
+ window.app = new App();
284
+ window.app.init();
285
+ });