e-pick 1.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,81 +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
-
6
- const cli = meow(`
7
- Usage
8
- $ epick [options] <file_path>
9
-
10
- <file_path> - Path to the CSV file containing commit and repository information.
11
-
12
- Options
13
- --commit-index, -c Index of the commit ID column in the CSV file. Default is 5.
14
- --repo-index, -r Index of the repository name column in the CSV file. Default is 1.
15
- --repo-name, -n Name of the repository to filter commits by. If empty, the first valid repository name found will be used.
16
- --execute, -x Execute the cherry-pick command. Without this flag, the command will only simulate execution.
17
- --allow-empty-commit, -a Allows empty commits. Default is false.
18
- --verbose, -v Print detailed logs.
19
-
20
- Examples
21
- $ epick --commit-index 3 --repo-index 2 --repo-name "your-repo" --execute ./commits.csv
22
- Executes the cherry-pick command on the commit of the repository named "your-repo".
23
-
24
- $ epick -c 4 -r 1 -n "example-repo" ./commits.csv
25
- Simulates the cherry-pick command for "example-repo".
26
- `, {
27
- importMeta: import.meta,
28
- flags: {
29
- commitIndex: {
30
- type: 'number',
31
- shortFlag: 'c',
32
- default: 5,
33
- },
34
- repoIndex: {
35
- type: 'number',
36
- shortFlag: 'r',
37
- default: 1,
38
- },
39
- repoName: {
40
- type: 'string',
41
- shortFlag: 'n',
42
- default: ''
43
- },
44
- execute: {
45
- type: 'boolean',
46
- shortFlag: 'x'
47
- },
48
- allowEmptyCommit: {
49
- type: 'boolean',
50
- shortFlag: 'a',
51
- default: false
52
- },
53
- verbose: {
54
- type: 'boolean',
55
- shortFlag: 'v',
56
- default: false
57
- },
58
- },
59
- allowUnknownFlags: false
60
- });
61
-
62
- if (cli.input.length === 0) {
63
- console.error("Error: No file path provided. Please specify the path to the CSV file.");
64
- process.exit(1);
65
- }
66
-
67
- const filePath = cli.input[0];
68
-
69
- if (!fs.existsSync(filePath)) {
70
- console.error(`Error: The file "${filePath}" does not exist.`);
71
- process.exit(1);
72
- }
73
-
74
- const options = cli.flags;
75
-
76
- if (options.verbose) {
77
- console.log("Options:", options);
78
- console.log("Processing file:", filePath);
79
- }
80
-
81
- await pickCommit(filePath, options);
@@ -1,162 +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
- // Change the current working directory to the repository path
12
- const originalCwd = process.cwd();
13
- process.chdir(repoPath);
14
-
15
- // Execute the git command to check the commit type
16
- const result = execSync(`git cat-file -t ${commitId}`, { stdio: 'pipe' }).toString().trim();
17
-
18
- // Change back to the original working directory
19
- process.chdir(originalCwd);
20
-
21
- // If the command's output starts with "commit", the ID is valid
22
- return result === 'commit';
23
- } catch (error) {
24
- if (error.message.includes('fatal: git cat-file: could not get object info')) {
25
- return false;
26
- }
27
- console.error(error.message);
28
- process.exit(error.status || 128);
29
- }
30
- }
31
-
32
- function getSheetData({ file_path, commit_index, repo_index }) {
33
- // Read the CSV file
34
- let csv;
35
- try {
36
- csv = fs.readFileSync(file_path, 'utf8');
37
- } catch (err) {
38
- console.error(`Failed to read file ${file_path}`, err);
39
- process.exit(1);
40
- }
41
-
42
- // Split the CSV data into an array of arrays
43
- const rawData = csv
44
- .split('\n')
45
- .map((line) => line.split('\t').map((item) => item?.trim()))
46
- .filter((line) => line.filter(Boolean).length > 2);
47
-
48
- const rows = rawData.map((line) => ({
49
- commit_id: line[commit_index],
50
- repo_name: line[repo_index],
51
- }));
52
-
53
- if (!isValidCommit(rows[0]?.commit_id)) {
54
- rows.shift(); // Remove header row
55
- }
56
- return rows;
57
- }
58
-
59
- function isMatchRepoName(row, selected_repo) {
60
- return row?.repo_name?.toLowerCase() === selected_repo.toLowerCase();
61
- }
62
-
63
- /**
64
- * Extracts and returns the repository name from the provided object.
65
- *
66
- * @param {Object} params - The parameters object.
67
- * @param {string} params.repo_name - The name of the repository to be extracted.
68
- * @param {Array.<{commit_id: string, repo_name: string}>} params.data - An array of objects,
69
- * each containing a `commit_id` and a `repo_name`. This parameter is not used in the current implementation.
70
- *
71
- * @returns {string} The repository name extracted from the `repo_name` parameter.
72
- */
73
- function getRepoName({ repo_name, data }) {
74
- if (repo_name) {
75
- return repo_name;
76
- }
77
- const allRepos = uniqueArray(data.map((row) => row.repo_name).filter(Boolean));
78
-
79
- const matchRepos = allRepos.map((name) => {
80
- const commitId = data.find((row) => isMatchRepoName(row, name) && row?.commit_id)?.commit_id;
81
-
82
- const isExist = isExistCommit(commitId, process.cwd());
83
- return isExist ? name : null;
84
- });
85
-
86
- const validRepoName = matchRepos.find(Boolean);
87
-
88
- if (!validRepoName) {
89
- console.error('Invalid repo name', 'Please check repoName', allRepos);
90
- process.exit(1);
91
- }
92
-
93
- return validRepoName;
94
- }
95
-
96
- /**
97
- * Processes a CSV file containing commit and repository information to generate and optionally execute a git cherry-pick command.
98
- *
99
- * This function reads a CSV file specified by the `filePath` parameter, filters the data based on the provided options,
100
- * and generates a git cherry-pick command for the commits in the selected repository. If the `execute` option is true,
101
- * the cherry-pick command is executed in the current repository.
102
- *
103
- * @param {string} filePath - The path to the CSV file containing commit and repository information.
104
- * @param {Object} options - An object containing options for processing the CSV file.
105
- * @param {number} options.commitIndex - The index of the commit ID column in the CSV file.
106
- * @param {number} options.repoIndex - The index of the repository name column in the CSV file.
107
- * @param {string} options.repoName - The name of the repository to filter commits by. If empty, the first valid repository name found will be used.
108
- * @param {boolean} options.execute - If true, the generated cherry-pick command will be executed.
109
- * @param {boolean} options.allowEmptyCommit - If true, allows do not throw error empty commits.
110
- */
111
- export default async function pickCommit(filePath, options) {
112
- const { commitIndex, repoIndex, repoName, execute, allowEmptyCommit } = options;
113
- // Read the CSV file
114
- const data = getSheetData({ file_path: filePath, commit_index: commitIndex, repo_index: repoIndex });
115
-
116
- const selectedRepoName = getRepoName({ repo_name: repoName, data });
117
-
118
- // Check for invalid repo index
119
- const dataOfRepo = data.filter((line) => isMatchRepoName(line, selectedRepoName));
120
-
121
- // Check for invalid commit hashes
122
- const invalidCommit = dataOfRepo.find((line) => {
123
- const commitId = line.commit_id;
124
- return !isValidCommit(commitId) && (!allowEmptyCommit || commitId);
125
- });
126
-
127
- if (invalidCommit) {
128
- console.error('Invalid commit', 'Please check commitIndex', invalidCommit);
129
- process.exit(1);
130
- }
131
-
132
- // Extract commit hashes
133
- const commits = dataOfRepo.map((line) => line.commit_id);
134
-
135
- // Run git show command for each commit
136
- const command = `git show -s --format="%ci %H" ${commits.join(' ')}`;
137
-
138
- let output;
139
- try {
140
- output = execSync(command, { encoding: 'utf8' }).trim();
141
- } catch (err) {
142
- console.error(`Failed to execute command ${command}`, err);
143
- process.exit(1);
144
- }
145
-
146
- // Sort the output data by commit date and hash
147
- const outputData = output
148
- .split('\n')
149
- .sort()
150
- .map((line) => line.split(' '));
151
-
152
- // Extract sorted commit hashes
153
- const sortedCommits = outputData.map((line) => line[3]);
154
-
155
- // Generate cherry-pick command
156
- const pickCommand = `git cherry-pick ${sortedCommits.join(' ')}`;
157
- console.log(pickCommand);
158
-
159
- if (execute) {
160
- execSync(pickCommand, { stdio: 'inherit' });
161
- }
162
- }