e-pick 1.0.0 → 2.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/cli.js +45 -15
- package/lib/pick-commit.js +3 -0
- package/package.json +4 -3
- package/public/index.html +44 -0
- package/public/script.js +263 -0
- package/public/styles.css +179 -0
- package/server.js +154 -0
package/cli.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import meow from 'meow';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import pickCommit from './lib/pick-commit.js';
|
|
5
|
+
import { startServer } from './server.js';
|
|
5
6
|
|
|
7
|
+
// Define CLI options and usage
|
|
6
8
|
const cli = meow(`
|
|
7
9
|
Usage
|
|
8
10
|
$ epick [options] <file_path>
|
|
@@ -16,6 +18,8 @@ Options
|
|
|
16
18
|
--execute, -x Execute the cherry-pick command. Without this flag, the command will only simulate execution.
|
|
17
19
|
--allow-empty-commit, -a Allows empty commits. Default is false.
|
|
18
20
|
--verbose, -v Print detailed logs.
|
|
21
|
+
--serve, -s Start the server to serve the pick code UI.
|
|
22
|
+
--port, -p Port to run the server on. Default is 3000.
|
|
19
23
|
|
|
20
24
|
Examples
|
|
21
25
|
$ epick --commit-index 3 --repo-index 2 --repo-name "your-repo" --execute ./commits.csv
|
|
@@ -55,27 +59,53 @@ Examples
|
|
|
55
59
|
shortFlag: 'v',
|
|
56
60
|
default: false
|
|
57
61
|
},
|
|
62
|
+
serve: {
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
shortFlag: 's',
|
|
65
|
+
default: false,
|
|
66
|
+
},
|
|
67
|
+
port: {
|
|
68
|
+
type: 'number',
|
|
69
|
+
shortFlag: 'p',
|
|
70
|
+
default: 3000,
|
|
71
|
+
},
|
|
58
72
|
},
|
|
59
73
|
allowUnknownFlags: false
|
|
60
74
|
});
|
|
61
75
|
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
76
|
+
// Start the server if the --serve flag is provided
|
|
77
|
+
if (cli.flags.serve) {
|
|
78
|
+
startServer(cli.flags.port);
|
|
79
|
+
} else {
|
|
80
|
+
// Ensure a file path is provided
|
|
81
|
+
if (cli.input.length === 0) {
|
|
82
|
+
console.error("Error: No file path provided. Please specify the path to the CSV file.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
66
85
|
|
|
67
|
-
const filePath = cli.input[0];
|
|
86
|
+
const filePath = cli.input[0];
|
|
68
87
|
|
|
69
|
-
if
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
// Check if the file exists
|
|
89
|
+
if (!fs.existsSync(filePath)) {
|
|
90
|
+
console.error(`Error: The file "${filePath}" does not exist.`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
73
93
|
|
|
74
|
-
const options = cli.flags;
|
|
94
|
+
const options = cli.flags;
|
|
75
95
|
|
|
76
|
-
if
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
// Log options and file path if verbose flag is set
|
|
97
|
+
if (options.verbose) {
|
|
98
|
+
console.log("Options:", options);
|
|
99
|
+
console.log("Processing file:", filePath);
|
|
100
|
+
}
|
|
80
101
|
|
|
81
|
-
|
|
102
|
+
// Process the file and execute the pickCommit function
|
|
103
|
+
(async () => {
|
|
104
|
+
try {
|
|
105
|
+
await pickCommit(filePath, options);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Error processing the file:", error);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
}
|
package/lib/pick-commit.js
CHANGED
|
@@ -8,6 +8,9 @@ const uniqueArray = (array) => [...new Set(array)];
|
|
|
8
8
|
|
|
9
9
|
function isExistCommit(commitId, repoPath) {
|
|
10
10
|
try {
|
|
11
|
+
if (!isValidCommit(commitId)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
11
14
|
// Change the current working directory to the repository path
|
|
12
15
|
const originalCwd = process.cwd();
|
|
13
16
|
process.chdir(repoPath);
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "e-pick",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "",
|
|
5
|
-
"scripts": {
|
|
6
|
-
},
|
|
5
|
+
"scripts": {},
|
|
7
6
|
"bin": {
|
|
8
7
|
"epick": "./cli.js"
|
|
9
8
|
},
|
|
@@ -12,6 +11,8 @@
|
|
|
12
11
|
"type": "module",
|
|
13
12
|
"license": "MIT",
|
|
14
13
|
"dependencies": {
|
|
14
|
+
"express": "^4.21.0",
|
|
15
|
+
"joi": "^17.13.3",
|
|
15
16
|
"meow": "^13.2.0"
|
|
16
17
|
}
|
|
17
18
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
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">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<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>
|
|
31
|
+
</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">×</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>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
package/public/script.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const COMMIT_HASH_REGEX = /^[0-9a-f]{40}$/;
|
|
2
|
+
const isValidCommit = (commit_id) => COMMIT_HASH_REGEX.test(commit_id);
|
|
3
|
+
|
|
4
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
5
|
+
const fileInput = document.getElementById('fileInput');
|
|
6
|
+
const loading = document.getElementById('loading');
|
|
7
|
+
const searchContainer = document.getElementById('searchContainer');
|
|
8
|
+
const searchBox = document.getElementById('searchBox');
|
|
9
|
+
const sheetSuggestions = document.getElementById('sheetSuggestions');
|
|
10
|
+
const selectedSheetList = document.getElementById('selectedSheetList');
|
|
11
|
+
const tableContainer = document.getElementById('tableContainer');
|
|
12
|
+
const submitContainer = document.getElementById('submitContainer');
|
|
13
|
+
const headerSelector1 = document.getElementById('headerSelector1');
|
|
14
|
+
const headerSelector2 = document.getElementById('headerSelector2');
|
|
15
|
+
let workbook;
|
|
16
|
+
|
|
17
|
+
fileInput.addEventListener('change', () => {
|
|
18
|
+
console.log('File selected:', fileInput.files[0].name);
|
|
19
|
+
if (fileInput.files.length) {
|
|
20
|
+
uploadFile(fileInput.files[0]);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function uploadFile(file) {
|
|
25
|
+
console.log('Uploading file:', file.name);
|
|
26
|
+
const reader = new FileReader();
|
|
27
|
+
|
|
28
|
+
reader.onload = (e) => {
|
|
29
|
+
const data = new Uint8Array(e.target.result);
|
|
30
|
+
workbook = XLSX.read(data, { type: 'array' });
|
|
31
|
+
console.log('Workbook loaded:', workbook);
|
|
32
|
+
|
|
33
|
+
// Hide loading indicator
|
|
34
|
+
loading.style.display = 'none';
|
|
35
|
+
|
|
36
|
+
// Show search box
|
|
37
|
+
searchContainer.style.display = 'block';
|
|
38
|
+
|
|
39
|
+
// Populate datalist with sheet names and valid row counts
|
|
40
|
+
sheetSuggestions.innerHTML = '';
|
|
41
|
+
workbook.SheetNames.forEach(sheet => {
|
|
42
|
+
const sheetData = XLSX.utils.sheet_to_json(workbook.Sheets[sheet]);
|
|
43
|
+
const validRowCount = sheetData.filter(row => {
|
|
44
|
+
const validCellCount = Object.values(row).filter(value => value !== undefined && value !== '').length;
|
|
45
|
+
return validCellCount >= 3;
|
|
46
|
+
}).length;
|
|
47
|
+
|
|
48
|
+
const option = document.createElement('option');
|
|
49
|
+
option.value = `${sheet} (Valid Rows: ${validRowCount})`;
|
|
50
|
+
sheetSuggestions.appendChild(option);
|
|
51
|
+
console.log(`Sheet: ${sheet}, Valid Rows: ${validRowCount}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Handle sheet selection
|
|
55
|
+
searchBox.addEventListener('change', () => {
|
|
56
|
+
const selectedSheet = searchBox.value.split(' (')[0]; // Extract sheet name
|
|
57
|
+
if (selectedSheet && !Array.from(selectedSheetList.children).some(item => item.textContent.includes(selectedSheet))) {
|
|
58
|
+
console.log('Sheet selected:', selectedSheet);
|
|
59
|
+
const listItem = document.createElement('li');
|
|
60
|
+
listItem.textContent = selectedSheet;
|
|
61
|
+
|
|
62
|
+
// Create remove button
|
|
63
|
+
const removeButton = document.createElement('button');
|
|
64
|
+
removeButton.textContent = 'x';
|
|
65
|
+
removeButton.style.marginLeft = '10px';
|
|
66
|
+
removeButton.addEventListener('click', () => {
|
|
67
|
+
console.log('Removing sheet:', selectedSheet);
|
|
68
|
+
selectedSheetList.removeChild(listItem);
|
|
69
|
+
updateTable();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
listItem.appendChild(removeButton);
|
|
73
|
+
selectedSheetList.appendChild(listItem);
|
|
74
|
+
searchBox.value = ''; // Clear the search box
|
|
75
|
+
updateTable();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Show loading indicator
|
|
81
|
+
loading.style.display = 'block';
|
|
82
|
+
searchContainer.style.display = 'none';
|
|
83
|
+
selectedSheetList.innerHTML = ''; // Clear selected sheets
|
|
84
|
+
tableContainer.innerHTML = ''; // Clear previous table
|
|
85
|
+
submitContainer.innerHTML = ''; // Clear previous submit button
|
|
86
|
+
|
|
87
|
+
reader.readAsArrayBuffer(file);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Update the table with merged data from selected sheets
|
|
91
|
+
function updateTable() {
|
|
92
|
+
const selectedSheets = Array.from(selectedSheetList.children).map(item => item.textContent.replace('x', '').trim());
|
|
93
|
+
let mergedData = [];
|
|
94
|
+
let validRowCounts = {};
|
|
95
|
+
|
|
96
|
+
selectedSheets.forEach(sheetName => {
|
|
97
|
+
console.log(`Processing sheet: ${sheetName}`);
|
|
98
|
+
const sheet = workbook.Sheets[sheetName];
|
|
99
|
+
let sheetData = XLSX.utils.sheet_to_json(sheet);
|
|
100
|
+
|
|
101
|
+
// Filter out rows with fewer than 3 valid (non-empty) cells
|
|
102
|
+
sheetData = sheetData.filter(row => {
|
|
103
|
+
const validCellCount = Object.values(row).filter(value => value !== undefined && value !== '').length;
|
|
104
|
+
const isValid = validCellCount >= 3;
|
|
105
|
+
console.log(`Row: ${JSON.stringify(row)}, Valid Cells: ${validCellCount}, Valid: ${isValid}`);
|
|
106
|
+
return isValid;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
validRowCounts[sheetName] = sheetData.length;
|
|
110
|
+
console.log(`Valid rows in sheet ${sheetName}: ${validRowCounts[sheetName]}`);
|
|
111
|
+
mergedData = mergedData.concat(sheetData);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log('Merged Data:', mergedData);
|
|
115
|
+
displayTable(mergedData, validRowCounts);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Display merged data in a table
|
|
119
|
+
function displayTable(data, validRowCounts) {
|
|
120
|
+
tableContainer.innerHTML = ''; // Clear previous table
|
|
121
|
+
submitContainer.innerHTML = ''; // Clear previous submit button
|
|
122
|
+
|
|
123
|
+
if (data.length === 0) {
|
|
124
|
+
tableContainer.textContent = 'No valid data to display.';
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const table = document.createElement('table');
|
|
129
|
+
const thead = document.createElement('thead');
|
|
130
|
+
const tbody = document.createElement('tbody');
|
|
131
|
+
|
|
132
|
+
// Create table headers
|
|
133
|
+
const headers = Object.keys(data[0]);
|
|
134
|
+
const headerRow = document.createElement('tr');
|
|
135
|
+
headers.forEach(header => {
|
|
136
|
+
const th = document.createElement('th');
|
|
137
|
+
th.textContent = header;
|
|
138
|
+
headerRow.appendChild(th);
|
|
139
|
+
});
|
|
140
|
+
thead.appendChild(headerRow);
|
|
141
|
+
|
|
142
|
+
// Populate header selectors
|
|
143
|
+
headerSelector1.innerHTML = '';
|
|
144
|
+
headerSelector2.innerHTML = '';
|
|
145
|
+
headers.forEach(header => {
|
|
146
|
+
const option1 = document.createElement('option');
|
|
147
|
+
option1.value = header;
|
|
148
|
+
option1.textContent = header;
|
|
149
|
+
headerSelector1.appendChild(option1);
|
|
150
|
+
|
|
151
|
+
const option2 = document.createElement('option');
|
|
152
|
+
option2.value = header;
|
|
153
|
+
option2.textContent = header;
|
|
154
|
+
headerSelector2.appendChild(option2);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Set default values for header selectors
|
|
158
|
+
const defaultRepoHeader = headers.find(header => header.toLowerCase().includes('repo'));
|
|
159
|
+
const defaultCommitHeader = headers.find(header => header.toLowerCase().includes('commit'));
|
|
160
|
+
|
|
161
|
+
if (defaultRepoHeader) {
|
|
162
|
+
headerSelector1.value = defaultRepoHeader;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (defaultCommitHeader) {
|
|
166
|
+
headerSelector2.value = defaultCommitHeader;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Enable header selectors
|
|
170
|
+
headerSelector1.disabled = false;
|
|
171
|
+
headerSelector2.disabled = false;
|
|
172
|
+
|
|
173
|
+
// Create table rows
|
|
174
|
+
data.forEach(row => {
|
|
175
|
+
const tr = document.createElement('tr');
|
|
176
|
+
headers.forEach(header => {
|
|
177
|
+
const td = document.createElement('td');
|
|
178
|
+
td.textContent = row[header] || '';
|
|
179
|
+
tr.appendChild(td);
|
|
180
|
+
});
|
|
181
|
+
tbody.appendChild(tr);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
table.appendChild(thead);
|
|
185
|
+
table.appendChild(tbody);
|
|
186
|
+
tableContainer.appendChild(table);
|
|
187
|
+
|
|
188
|
+
// Add title with valid row counts
|
|
189
|
+
const title = document.createElement('h2');
|
|
190
|
+
title.textContent = 'Merged Data (Valid Rows: ' + Object.entries(validRowCounts).map(([sheet, count]) => `${sheet}: ${count}`).join(', ') + ')';
|
|
191
|
+
tableContainer.insertBefore(title, table);
|
|
192
|
+
|
|
193
|
+
// Create and display submit button if there is valid data
|
|
194
|
+
if (data.length > 0) {
|
|
195
|
+
const submitButton = document.createElement('button');
|
|
196
|
+
submitButton.textContent = 'Submit';
|
|
197
|
+
submitButton.addEventListener('click', () => {
|
|
198
|
+
console.log('Submit button clicked');
|
|
199
|
+
submitData(data);
|
|
200
|
+
});
|
|
201
|
+
submitContainer.appendChild(submitButton);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Submit data to API
|
|
206
|
+
function submitData(data) {
|
|
207
|
+
const repoColumn = headerSelector1.value;
|
|
208
|
+
const commitColumn = headerSelector2.value;
|
|
209
|
+
|
|
210
|
+
const payload = data.map(row => ({
|
|
211
|
+
repo: row[repoColumn],
|
|
212
|
+
commit_id: row[commitColumn]
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
console.log('Payload:', payload);
|
|
216
|
+
|
|
217
|
+
fetch('/api/submit', {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: {
|
|
220
|
+
'Content-Type': 'application/json'
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify(payload)
|
|
223
|
+
})
|
|
224
|
+
.then(response => response.json())
|
|
225
|
+
.then(result => {
|
|
226
|
+
if (result.error) {
|
|
227
|
+
showModal(result.error, false);
|
|
228
|
+
} else {
|
|
229
|
+
console.log('Success:', result);
|
|
230
|
+
showModal(result.command, true);
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
.catch(error => {
|
|
234
|
+
console.error('Error:', error);
|
|
235
|
+
showModal(error.message, false);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Show modal with message and copy button
|
|
240
|
+
function showModal(message, isSuccess) {
|
|
241
|
+
const modal = document.getElementById('modal');
|
|
242
|
+
const modalMessage = document.getElementById('modalMessage');
|
|
243
|
+
const copyButton = document.getElementById('copyButton');
|
|
244
|
+
|
|
245
|
+
modalMessage.textContent = message;
|
|
246
|
+
copyButton.style.display = isSuccess ? 'block' : 'none';
|
|
247
|
+
|
|
248
|
+
modal.style.display = 'block';
|
|
249
|
+
|
|
250
|
+
copyButton.onclick = () => {
|
|
251
|
+
navigator.clipboard.writeText(message).then(() => {
|
|
252
|
+
alert('Command copied to clipboard!');
|
|
253
|
+
}).catch(err => {
|
|
254
|
+
console.error('Failed to copy command:', err);
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Close modal when clicking the close button
|
|
260
|
+
document.getElementById('closeButton').onclick = () => {
|
|
261
|
+
document.getElementById('modal').style.display = 'none';
|
|
262
|
+
};
|
|
263
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/* General Styles */
|
|
2
|
+
body {
|
|
3
|
+
font-family: Arial, sans-serif;
|
|
4
|
+
background-color: #f4f4f9;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
display: flex;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
align-items: center;
|
|
10
|
+
height: 100vh;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.container {
|
|
14
|
+
background-color: #fff;
|
|
15
|
+
padding: 20px;
|
|
16
|
+
border-radius: 8px;
|
|
17
|
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
18
|
+
width: 90%;
|
|
19
|
+
max-width: 600px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
h1, h2 {
|
|
23
|
+
color: #333;
|
|
24
|
+
text-align: center;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
input[type="file"] {
|
|
28
|
+
display: block;
|
|
29
|
+
margin: 20px auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
button {
|
|
33
|
+
display: block;
|
|
34
|
+
width: 100%;
|
|
35
|
+
padding: 10px;
|
|
36
|
+
margin: 10px 0;
|
|
37
|
+
border: none;
|
|
38
|
+
border-radius: 4px;
|
|
39
|
+
background-color: #007bff;
|
|
40
|
+
color: white;
|
|
41
|
+
font-size: 16px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
transition: background-color 0.3s ease;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
button:hover {
|
|
47
|
+
background-color: #0056b3;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
button:disabled {
|
|
51
|
+
background-color: #cccccc; /* Light gray background */
|
|
52
|
+
cursor: not-allowed; /* Change cursor to not-allowed */
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
input[type="text"], select {
|
|
56
|
+
width: 100%;
|
|
57
|
+
padding: 10px;
|
|
58
|
+
margin: 10px 0;
|
|
59
|
+
border: 1px solid #ccc;
|
|
60
|
+
border-radius: 4px;
|
|
61
|
+
box-sizing: border-box;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
input[type="text"]:focus, select:focus {
|
|
65
|
+
border-color: #007bff;
|
|
66
|
+
outline: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ul {
|
|
70
|
+
list-style-type: none;
|
|
71
|
+
padding: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ul li {
|
|
75
|
+
background-color: #f9f9f9;
|
|
76
|
+
margin: 5px 0;
|
|
77
|
+
padding: 10px;
|
|
78
|
+
border: 1px solid #ddd;
|
|
79
|
+
border-radius: 4px;
|
|
80
|
+
display: flex;
|
|
81
|
+
justify-content: space-between;
|
|
82
|
+
align-items: center;
|
|
83
|
+
position: relative;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ul li button {
|
|
87
|
+
background-color: #dc3545;
|
|
88
|
+
color: white;
|
|
89
|
+
border: none;
|
|
90
|
+
border-radius: 4px;
|
|
91
|
+
padding: 5px 10px;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
transition: background-color 0.3s ease;
|
|
94
|
+
position: absolute;
|
|
95
|
+
right: 10px;
|
|
96
|
+
width: 30px;
|
|
97
|
+
text-align: center;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ul li button:hover {
|
|
101
|
+
background-color: #c82333;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
table {
|
|
105
|
+
width: 100%;
|
|
106
|
+
border-collapse: collapse;
|
|
107
|
+
margin: 20px 0;
|
|
108
|
+
max-height: 400px;
|
|
109
|
+
overflow-y: auto;
|
|
110
|
+
display: block;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
table, th, td {
|
|
114
|
+
border: 1px solid #ddd;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
th, td {
|
|
118
|
+
padding: 10px;
|
|
119
|
+
text-align: left;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
th {
|
|
123
|
+
background-color: #f2f2f2;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#loading {
|
|
127
|
+
text-align: center;
|
|
128
|
+
font-size: 18px;
|
|
129
|
+
color: #007bff;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
/* Modal styles */
|
|
135
|
+
.modal {
|
|
136
|
+
display: none;
|
|
137
|
+
position: fixed;
|
|
138
|
+
z-index: 1;
|
|
139
|
+
left: 0;
|
|
140
|
+
top: 0;
|
|
141
|
+
width: 100%;
|
|
142
|
+
height: 100%;
|
|
143
|
+
overflow: auto;
|
|
144
|
+
background-color: rgb(0,0,0);
|
|
145
|
+
background-color: rgba(0,0,0,0.4);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.modal-content {
|
|
149
|
+
background-color: #fefefe;
|
|
150
|
+
margin: 15% auto;
|
|
151
|
+
padding: 20px;
|
|
152
|
+
border: 1px solid #888;
|
|
153
|
+
width: 80%;
|
|
154
|
+
max-width: 500px;
|
|
155
|
+
text-align: center;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.close-button {
|
|
159
|
+
color: #aaa;
|
|
160
|
+
float: right;
|
|
161
|
+
font-size: 28px;
|
|
162
|
+
font-weight: bold;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.close-button:hover,
|
|
166
|
+
.close-button:focus {
|
|
167
|
+
color: black;
|
|
168
|
+
text-decoration: none;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#copyButton {
|
|
173
|
+
margin-top: 20px;
|
|
174
|
+
background-color: #28a745;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#copyButton:hover {
|
|
178
|
+
background-color: #218838;
|
|
179
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import net from 'net';
|
|
5
|
+
import Joi from 'joi';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
const app = express();
|
|
9
|
+
|
|
10
|
+
// Resolve the path to the "public" directory
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const publicDirectoryPath = path.resolve(__dirname, 'public');
|
|
14
|
+
|
|
15
|
+
// Serve static files from the "public" directory
|
|
16
|
+
app.use(express.static(publicDirectoryPath));
|
|
17
|
+
|
|
18
|
+
// Middleware to parse JSON payloads
|
|
19
|
+
app.use(express.json());
|
|
20
|
+
|
|
21
|
+
const COMMIT_HASH_REGEX = /^[0-9a-f]{40}$/;
|
|
22
|
+
const isValidCommit = (commit_id) => COMMIT_HASH_REGEX.test(commit_id);
|
|
23
|
+
|
|
24
|
+
function isExistCommit(commitId, repoPath) {
|
|
25
|
+
try {
|
|
26
|
+
if (!isValidCommit(commitId)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// Change the current working directory to the repository path
|
|
30
|
+
const originalCwd = process.cwd();
|
|
31
|
+
process.chdir(repoPath);
|
|
32
|
+
|
|
33
|
+
// Execute the git command to check the commit type
|
|
34
|
+
const result = execSync(`git cat-file -t ${commitId}`, { stdio: 'pipe' }).toString().trim();
|
|
35
|
+
|
|
36
|
+
// Change back to the original working directory
|
|
37
|
+
process.chdir(originalCwd);
|
|
38
|
+
|
|
39
|
+
// If the command's output starts with "commit", the ID is valid
|
|
40
|
+
return result === 'commit';
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`Error checking commit ID: ${error.message}`);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const uniqueArray = (array) => [...new Set(array)];
|
|
48
|
+
|
|
49
|
+
function getRepoName(data) {
|
|
50
|
+
const allRepos = uniqueArray(data.map((row) => row.repo).filter(Boolean));
|
|
51
|
+
|
|
52
|
+
return allRepos.find((name) => {
|
|
53
|
+
const commitId = data.find((row) => row.repo === name && row?.commit_id)?.commit_id;
|
|
54
|
+
return isExistCommit(commitId, process.cwd());
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Validation schema using Joi
|
|
59
|
+
const schema = Joi.array().items(
|
|
60
|
+
Joi.object({
|
|
61
|
+
repo: Joi.string().required(),
|
|
62
|
+
commit_id: Joi.string()
|
|
63
|
+
.pattern(/^[0-9a-f]{40}$/)
|
|
64
|
+
.required(),
|
|
65
|
+
})
|
|
66
|
+
).min(1);
|
|
67
|
+
|
|
68
|
+
const orderCommitIds = (commits) => {
|
|
69
|
+
// Run git show command for each commit
|
|
70
|
+
const command = `git show -s --format="%ci %H" ${commits.join(' ')}`;
|
|
71
|
+
|
|
72
|
+
let output;
|
|
73
|
+
try {
|
|
74
|
+
output = execSync(command, { encoding: 'utf8' }).trim();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return { error: err.message };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Sort the output data by commit date and hash
|
|
80
|
+
const outputData = output
|
|
81
|
+
.split('\n')
|
|
82
|
+
.sort()
|
|
83
|
+
.map((line) => line.split(' '));
|
|
84
|
+
|
|
85
|
+
// Extract sorted commit hashes
|
|
86
|
+
const sortedCommits = outputData.map((line) => line[3]);
|
|
87
|
+
|
|
88
|
+
// Generate cherry-pick command
|
|
89
|
+
const pickCommand = `git cherry-pick ${sortedCommits.join(' ')}`;
|
|
90
|
+
|
|
91
|
+
return { data: pickCommand };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Handle file upload and process Excel file
|
|
95
|
+
app.post('/api/submit', async (req, res) => {
|
|
96
|
+
const { error, value } = schema.validate(req.body);
|
|
97
|
+
if (error) {
|
|
98
|
+
return res.status(400).send({ error: `Invalid data format: ${error.details[0].message}` });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const repoName = getRepoName(value);
|
|
102
|
+
|
|
103
|
+
if (!repoName) {
|
|
104
|
+
return res.status(404).send({ error: 'No valid repository found.' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const allMatchRepoCommitIds = value.filter((row) => row.repo === repoName).map((row) => row.commit_id);
|
|
108
|
+
const orderData = orderCommitIds(allMatchRepoCommitIds);
|
|
109
|
+
|
|
110
|
+
if (orderData.error) {
|
|
111
|
+
return res.status(500).send({ error: `Failed to execute command: ${orderData.error}` });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
res.status(200).json({ repo: repoName, command: orderData.data });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Check port availability
|
|
118
|
+
const checkPortAvailability = (port, callback) => {
|
|
119
|
+
const server = net.createServer();
|
|
120
|
+
server.once('error', (err) => {
|
|
121
|
+
if (err.code === 'EADDRINUSE') {
|
|
122
|
+
callback(false);
|
|
123
|
+
} else {
|
|
124
|
+
callback(err);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
server.once('listening', () => {
|
|
128
|
+
server.close();
|
|
129
|
+
callback(true);
|
|
130
|
+
});
|
|
131
|
+
server.listen(port);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Flag to track if the server has already been started
|
|
135
|
+
let serverStarted = false;
|
|
136
|
+
|
|
137
|
+
// Start the server
|
|
138
|
+
export const startServer = (port) => {
|
|
139
|
+
if (serverStarted) {
|
|
140
|
+
console.log('Server is already running.');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
checkPortAvailability(port, (isAvailable) => {
|
|
145
|
+
if (isAvailable) {
|
|
146
|
+
serverStarted = true;
|
|
147
|
+
app.listen(port, () => {
|
|
148
|
+
console.log(`Server is running on http://localhost:${port}`);
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
console.error(`Port ${port} is already in use.`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
};
|