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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit Validator (Chain of Responsibility Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Validates commits through a chain of validators.
|
|
5
|
+
* Each validator checks one aspect of the commit.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const COMMIT_HASH_REGEX = /^[0-9a-f]{40}$/i;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base validator class
|
|
12
|
+
*/
|
|
13
|
+
class BaseValidator {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.nextValidator = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Set the next validator in the chain
|
|
20
|
+
* @param {BaseValidator} validator - Next validator
|
|
21
|
+
* @returns {BaseValidator} The next validator
|
|
22
|
+
*/
|
|
23
|
+
setNext(validator) {
|
|
24
|
+
this.nextValidator = validator;
|
|
25
|
+
return validator;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate commit and pass to next validator
|
|
30
|
+
* @param {string} commitHash - Commit hash to validate
|
|
31
|
+
* @param {object} context - Validation context
|
|
32
|
+
* @returns {object} Validation result
|
|
33
|
+
*/
|
|
34
|
+
async validate(commitHash, context) {
|
|
35
|
+
const result = await this._validate(commitHash, context);
|
|
36
|
+
|
|
37
|
+
if (!result.isValid || !this.nextValidator) {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this.nextValidator.validate(commitHash, context);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Internal validation logic (to be implemented by subclasses)
|
|
46
|
+
* @param {string} commitHash - Commit hash
|
|
47
|
+
* @param {object} context - Validation context
|
|
48
|
+
* @returns {object} Validation result
|
|
49
|
+
* @protected
|
|
50
|
+
*/
|
|
51
|
+
async _validate(commitHash, context) {
|
|
52
|
+
throw new Error('_validate method must be implemented');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format Validator - Checks if commit hash has valid format
|
|
58
|
+
*/
|
|
59
|
+
class FormatValidator extends BaseValidator {
|
|
60
|
+
async _validate(commitHash, context) {
|
|
61
|
+
if (!commitHash || typeof commitHash !== 'string') {
|
|
62
|
+
return {
|
|
63
|
+
commit: commitHash,
|
|
64
|
+
isValid: false,
|
|
65
|
+
error: {
|
|
66
|
+
type: 'FORMAT_ERROR',
|
|
67
|
+
message: 'Commit hash is required and must be a string',
|
|
68
|
+
},
|
|
69
|
+
canIgnore: false,
|
|
70
|
+
commitInfo: null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const trimmedHash = commitHash.trim();
|
|
75
|
+
|
|
76
|
+
if (!COMMIT_HASH_REGEX.test(trimmedHash)) {
|
|
77
|
+
return {
|
|
78
|
+
commit: trimmedHash,
|
|
79
|
+
isValid: false,
|
|
80
|
+
error: {
|
|
81
|
+
type: 'FORMAT_ERROR',
|
|
82
|
+
message: 'Invalid commit hash format. Must be 40 hexadecimal characters',
|
|
83
|
+
},
|
|
84
|
+
canIgnore: false,
|
|
85
|
+
commitInfo: null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
commit: trimmedHash,
|
|
91
|
+
isValid: true,
|
|
92
|
+
error: null,
|
|
93
|
+
canIgnore: false,
|
|
94
|
+
commitInfo: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Existence Validator - Checks if commit exists in repository
|
|
101
|
+
*/
|
|
102
|
+
class ExistenceValidator extends BaseValidator {
|
|
103
|
+
async _validate(commitHash, context) {
|
|
104
|
+
const { gitService } = context;
|
|
105
|
+
|
|
106
|
+
if (!gitService) {
|
|
107
|
+
throw new Error('GitService is required in validation context');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const exists = gitService.validateCommit(commitHash);
|
|
111
|
+
|
|
112
|
+
if (!exists) {
|
|
113
|
+
return {
|
|
114
|
+
commit: commitHash,
|
|
115
|
+
isValid: false,
|
|
116
|
+
error: {
|
|
117
|
+
type: 'NOT_FOUND',
|
|
118
|
+
message: 'Commit does not exist in repository. It may not have been fetched from remote yet.',
|
|
119
|
+
},
|
|
120
|
+
canIgnore: true, // User can ignore if they know the commit should exist
|
|
121
|
+
commitInfo: null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Get commit info for display
|
|
126
|
+
const commitInfo = gitService.getCommitInfo(commitHash);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
commit: commitHash,
|
|
130
|
+
isValid: true,
|
|
131
|
+
error: null,
|
|
132
|
+
canIgnore: false,
|
|
133
|
+
commitInfo,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create the validation chain
|
|
140
|
+
* @returns {BaseValidator} The first validator in the chain
|
|
141
|
+
*/
|
|
142
|
+
export function createValidationChain() {
|
|
143
|
+
const formatValidator = new FormatValidator();
|
|
144
|
+
const existenceValidator = new ExistenceValidator();
|
|
145
|
+
|
|
146
|
+
formatValidator.setNext(existenceValidator);
|
|
147
|
+
|
|
148
|
+
return formatValidator;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validate a single commit
|
|
153
|
+
* @param {string} commitHash - Commit hash to validate
|
|
154
|
+
* @param {object} context - Validation context with gitService
|
|
155
|
+
* @returns {Promise<object>} Validation result
|
|
156
|
+
*/
|
|
157
|
+
export async function validateCommit(commitHash, context) {
|
|
158
|
+
const chain = createValidationChain();
|
|
159
|
+
return chain.validate(commitHash, context);
|
|
160
|
+
}
|
package/cli.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import meow from 'meow';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import pickCommit from './lib/pick-commit.js';
|
|
5
|
-
import { startServer } from './server.js';
|
|
6
|
-
|
|
7
|
-
// Define CLI options and usage
|
|
8
|
-
const cli = meow(`
|
|
9
|
-
Usage
|
|
10
|
-
$ epick [options] <file_path>
|
|
11
|
-
|
|
12
|
-
<file_path> - Path to the CSV file containing commit and repository information.
|
|
13
|
-
|
|
14
|
-
Options
|
|
15
|
-
--commit-index, -c Index of the commit ID column in the CSV file. Default is 5.
|
|
16
|
-
--repo-index, -r Index of the repository name column in the CSV file. Default is 1.
|
|
17
|
-
--repo-name, -n Name of the repository to filter commits by. If empty, the first valid repository name found will be used.
|
|
18
|
-
--execute, -x Execute the cherry-pick command. Without this flag, the command will only simulate execution.
|
|
19
|
-
--allow-empty-commit, -a Allows empty commits. Default is false.
|
|
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.
|
|
23
|
-
|
|
24
|
-
Examples
|
|
25
|
-
$ epick --commit-index 3 --repo-index 2 --repo-name "your-repo" --execute ./commits.csv
|
|
26
|
-
Executes the cherry-pick command on the commit of the repository named "your-repo".
|
|
27
|
-
|
|
28
|
-
$ epick -c 4 -r 1 -n "example-repo" ./commits.csv
|
|
29
|
-
Simulates the cherry-pick command for "example-repo".
|
|
30
|
-
`, {
|
|
31
|
-
importMeta: import.meta,
|
|
32
|
-
flags: {
|
|
33
|
-
commitIndex: {
|
|
34
|
-
type: 'number',
|
|
35
|
-
shortFlag: 'c',
|
|
36
|
-
default: 5,
|
|
37
|
-
},
|
|
38
|
-
repoIndex: {
|
|
39
|
-
type: 'number',
|
|
40
|
-
shortFlag: 'r',
|
|
41
|
-
default: 1,
|
|
42
|
-
},
|
|
43
|
-
repoName: {
|
|
44
|
-
type: 'string',
|
|
45
|
-
shortFlag: 'n',
|
|
46
|
-
default: ''
|
|
47
|
-
},
|
|
48
|
-
execute: {
|
|
49
|
-
type: 'boolean',
|
|
50
|
-
shortFlag: 'x'
|
|
51
|
-
},
|
|
52
|
-
allowEmptyCommit: {
|
|
53
|
-
type: 'boolean',
|
|
54
|
-
shortFlag: 'a',
|
|
55
|
-
default: false
|
|
56
|
-
},
|
|
57
|
-
verbose: {
|
|
58
|
-
type: 'boolean',
|
|
59
|
-
shortFlag: 'v',
|
|
60
|
-
default: false
|
|
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
|
-
},
|
|
72
|
-
},
|
|
73
|
-
allowUnknownFlags: false
|
|
74
|
-
});
|
|
75
|
-
|
|
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
|
-
}
|
|
85
|
-
|
|
86
|
-
const filePath = cli.input[0];
|
|
87
|
-
|
|
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
|
-
}
|
|
93
|
-
|
|
94
|
-
const options = cli.flags;
|
|
95
|
-
|
|
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
|
-
}
|
|
101
|
-
|
|
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
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
|
|
4
|
-
const COMMIT_HASH_REGEX = /^[0-9a-f]{40}$/;
|
|
5
|
-
const isValidCommit = (commit_id) => COMMIT_HASH_REGEX.test(commit_id);
|
|
6
|
-
|
|
7
|
-
const uniqueArray = (array) => [...new Set(array)];
|
|
8
|
-
|
|
9
|
-
function isExistCommit(commitId, repoPath) {
|
|
10
|
-
try {
|
|
11
|
-
if (!isValidCommit(commitId)) {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
// Change the current working directory to the repository path
|
|
15
|
-
const originalCwd = process.cwd();
|
|
16
|
-
process.chdir(repoPath);
|
|
17
|
-
|
|
18
|
-
// Execute the git command to check the commit type
|
|
19
|
-
const result = execSync(`git cat-file -t ${commitId}`, { stdio: 'pipe' }).toString().trim();
|
|
20
|
-
|
|
21
|
-
// Change back to the original working directory
|
|
22
|
-
process.chdir(originalCwd);
|
|
23
|
-
|
|
24
|
-
// If the command's output starts with "commit", the ID is valid
|
|
25
|
-
return result === 'commit';
|
|
26
|
-
} catch (error) {
|
|
27
|
-
if (error.message.includes('fatal: git cat-file: could not get object info')) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
console.error(error.message);
|
|
31
|
-
process.exit(error.status || 128);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function getSheetData({ file_path, commit_index, repo_index }) {
|
|
36
|
-
// Read the CSV file
|
|
37
|
-
let csv;
|
|
38
|
-
try {
|
|
39
|
-
csv = fs.readFileSync(file_path, 'utf8');
|
|
40
|
-
} catch (err) {
|
|
41
|
-
console.error(`Failed to read file ${file_path}`, err);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Split the CSV data into an array of arrays
|
|
46
|
-
const rawData = csv
|
|
47
|
-
.split('\n')
|
|
48
|
-
.map((line) => line.split('\t').map((item) => item?.trim()))
|
|
49
|
-
.filter((line) => line.filter(Boolean).length > 2);
|
|
50
|
-
|
|
51
|
-
const rows = rawData.map((line) => ({
|
|
52
|
-
commit_id: line[commit_index],
|
|
53
|
-
repo_name: line[repo_index],
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
if (!isValidCommit(rows[0]?.commit_id)) {
|
|
57
|
-
rows.shift(); // Remove header row
|
|
58
|
-
}
|
|
59
|
-
return rows;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function isMatchRepoName(row, selected_repo) {
|
|
63
|
-
return row?.repo_name?.toLowerCase() === selected_repo.toLowerCase();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Extracts and returns the repository name from the provided object.
|
|
68
|
-
*
|
|
69
|
-
* @param {Object} params - The parameters object.
|
|
70
|
-
* @param {string} params.repo_name - The name of the repository to be extracted.
|
|
71
|
-
* @param {Array.<{commit_id: string, repo_name: string}>} params.data - An array of objects,
|
|
72
|
-
* each containing a `commit_id` and a `repo_name`. This parameter is not used in the current implementation.
|
|
73
|
-
*
|
|
74
|
-
* @returns {string} The repository name extracted from the `repo_name` parameter.
|
|
75
|
-
*/
|
|
76
|
-
function getRepoName({ repo_name, data }) {
|
|
77
|
-
if (repo_name) {
|
|
78
|
-
return repo_name;
|
|
79
|
-
}
|
|
80
|
-
const allRepos = uniqueArray(data.map((row) => row.repo_name).filter(Boolean));
|
|
81
|
-
|
|
82
|
-
const matchRepos = allRepos.map((name) => {
|
|
83
|
-
const commitId = data.find((row) => isMatchRepoName(row, name) && row?.commit_id)?.commit_id;
|
|
84
|
-
|
|
85
|
-
const isExist = isExistCommit(commitId, process.cwd());
|
|
86
|
-
return isExist ? name : null;
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const validRepoName = matchRepos.find(Boolean);
|
|
90
|
-
|
|
91
|
-
if (!validRepoName) {
|
|
92
|
-
console.error('Invalid repo name', 'Please check repoName', allRepos);
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return validRepoName;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Processes a CSV file containing commit and repository information to generate and optionally execute a git cherry-pick command.
|
|
101
|
-
*
|
|
102
|
-
* This function reads a CSV file specified by the `filePath` parameter, filters the data based on the provided options,
|
|
103
|
-
* and generates a git cherry-pick command for the commits in the selected repository. If the `execute` option is true,
|
|
104
|
-
* the cherry-pick command is executed in the current repository.
|
|
105
|
-
*
|
|
106
|
-
* @param {string} filePath - The path to the CSV file containing commit and repository information.
|
|
107
|
-
* @param {Object} options - An object containing options for processing the CSV file.
|
|
108
|
-
* @param {number} options.commitIndex - The index of the commit ID column in the CSV file.
|
|
109
|
-
* @param {number} options.repoIndex - The index of the repository name column in the CSV file.
|
|
110
|
-
* @param {string} options.repoName - The name of the repository to filter commits by. If empty, the first valid repository name found will be used.
|
|
111
|
-
* @param {boolean} options.execute - If true, the generated cherry-pick command will be executed.
|
|
112
|
-
* @param {boolean} options.allowEmptyCommit - If true, allows do not throw error empty commits.
|
|
113
|
-
*/
|
|
114
|
-
export default async function pickCommit(filePath, options) {
|
|
115
|
-
const { commitIndex, repoIndex, repoName, execute, allowEmptyCommit } = options;
|
|
116
|
-
// Read the CSV file
|
|
117
|
-
const data = getSheetData({ file_path: filePath, commit_index: commitIndex, repo_index: repoIndex });
|
|
118
|
-
|
|
119
|
-
const selectedRepoName = getRepoName({ repo_name: repoName, data });
|
|
120
|
-
|
|
121
|
-
// Check for invalid repo index
|
|
122
|
-
const dataOfRepo = data.filter((line) => isMatchRepoName(line, selectedRepoName));
|
|
123
|
-
|
|
124
|
-
// Check for invalid commit hashes
|
|
125
|
-
const invalidCommit = dataOfRepo.find((line) => {
|
|
126
|
-
const commitId = line.commit_id;
|
|
127
|
-
return !isValidCommit(commitId) && (!allowEmptyCommit || commitId);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (invalidCommit) {
|
|
131
|
-
console.error('Invalid commit', 'Please check commitIndex', invalidCommit);
|
|
132
|
-
process.exit(1);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Extract commit hashes
|
|
136
|
-
const commits = dataOfRepo.map((line) => line.commit_id);
|
|
137
|
-
|
|
138
|
-
// Run git show command for each commit
|
|
139
|
-
const command = `git show -s --format="%ci %H" ${commits.join(' ')}`;
|
|
140
|
-
|
|
141
|
-
let output;
|
|
142
|
-
try {
|
|
143
|
-
output = execSync(command, { encoding: 'utf8' }).trim();
|
|
144
|
-
} catch (err) {
|
|
145
|
-
console.error(`Failed to execute command ${command}`, err);
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Sort the output data by commit date and hash
|
|
150
|
-
const outputData = output
|
|
151
|
-
.split('\n')
|
|
152
|
-
.sort()
|
|
153
|
-
.map((line) => line.split(' '));
|
|
154
|
-
|
|
155
|
-
// Extract sorted commit hashes
|
|
156
|
-
const sortedCommits = outputData.map((line) => line[3]);
|
|
157
|
-
|
|
158
|
-
// Generate cherry-pick command
|
|
159
|
-
const pickCommand = `git cherry-pick ${sortedCommits.join(' ')}`;
|
|
160
|
-
console.log(pickCommand);
|
|
161
|
-
|
|
162
|
-
if (execute) {
|
|
163
|
-
execSync(pickCommand, { stdio: 'inherit' });
|
|
164
|
-
}
|
|
165
|
-
}
|
package/public/script.js
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
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
|
-
});
|