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.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Cherry-Pick Command Builder (Builder Pattern)
3
+ *
4
+ * Provides a fluent interface for building cherry-pick command requests
5
+ * with various options.
6
+ */
7
+
8
+ class CherryPickCommandBuilder {
9
+ constructor() {
10
+ this._commits = [];
11
+ this._ignoredCommits = [];
12
+ this._options = {};
13
+ }
14
+
15
+ /**
16
+ * Set commits to cherry-pick
17
+ * @param {string[]} commits - Array of commit hashes
18
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
19
+ */
20
+ withCommits(commits) {
21
+ this._commits = Array.isArray(commits) ? commits : [commits];
22
+ return this;
23
+ }
24
+
25
+ /**
26
+ * Add a single commit
27
+ * @param {string} commit - Commit hash
28
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
29
+ */
30
+ addCommit(commit) {
31
+ this._commits.push(commit);
32
+ return this;
33
+ }
34
+
35
+ /**
36
+ * Set commits to ignore
37
+ * @param {string[]} commits - Array of commit hashes to ignore
38
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
39
+ */
40
+ ignoreCommits(commits) {
41
+ this._ignoredCommits = Array.isArray(commits) ? commits : [commits];
42
+ return this;
43
+ }
44
+
45
+ /**
46
+ * Add a single commit to ignore list
47
+ * @param {string} commit - Commit hash to ignore
48
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
49
+ */
50
+ ignoreCommit(commit) {
51
+ this._ignoredCommits.push(commit);
52
+ return this;
53
+ }
54
+
55
+ /**
56
+ * Allow empty commits in cherry-pick
57
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
58
+ */
59
+ allowEmpty() {
60
+ this._options.allowEmpty = true;
61
+ return this;
62
+ }
63
+
64
+ /**
65
+ * Set merge strategy
66
+ * @param {string} strategy - Merge strategy (e.g., 'recursive', 'ours', 'theirs')
67
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
68
+ */
69
+ withStrategy(strategy) {
70
+ this._options.strategy = strategy;
71
+ return this;
72
+ }
73
+
74
+ /**
75
+ * Set strategy option
76
+ * @param {string} option - Strategy option (e.g., 'patience', 'diff-algorithm=patience')
77
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
78
+ */
79
+ withStrategyOption(option) {
80
+ this._options.strategyOption = option;
81
+ return this;
82
+ }
83
+
84
+ /**
85
+ * Perform cherry-pick without committing
86
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
87
+ */
88
+ noCommit() {
89
+ this._options.noCommit = true;
90
+ return this;
91
+ }
92
+
93
+ /**
94
+ * Keep redundant commits (even if empty)
95
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
96
+ */
97
+ keepRedundant() {
98
+ this._options.keepRedundant = true;
99
+ return this;
100
+ }
101
+
102
+ /**
103
+ * Sign off the commit
104
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
105
+ */
106
+ signoff() {
107
+ this._options.signoff = true;
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * Set custom option
113
+ * @param {string} key - Option key
114
+ * @param {any} value - Option value
115
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
116
+ */
117
+ withOption(key, value) {
118
+ this._options[key] = value;
119
+ return this;
120
+ }
121
+
122
+ /**
123
+ * Build the request object
124
+ * @returns {object} Request object for API
125
+ */
126
+ build() {
127
+ if (this._commits.length === 0) {
128
+ throw new Error('No commits specified for cherry-pick');
129
+ }
130
+
131
+ return {
132
+ commits: this._commits,
133
+ ignoredCommits: this._ignoredCommits,
134
+ options: this._options
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Build and execute the command via API
140
+ * @param {APIFacade} api - API facade instance
141
+ * @returns {Promise<object>} Command generation result
142
+ */
143
+ async execute(api) {
144
+ const request = this.build();
145
+ return await api.generateCommand(
146
+ request.commits,
147
+ request.ignoredCommits,
148
+ request.options
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Reset builder to initial state
154
+ * @returns {CherryPickCommandBuilder} Builder instance for chaining
155
+ */
156
+ reset() {
157
+ this._commits = [];
158
+ this._ignoredCommits = [];
159
+ this._options = {};
160
+ return this;
161
+ }
162
+ }
163
+
164
+ // Export for use in other scripts
165
+ window.CherryPickCommandBuilder = CherryPickCommandBuilder;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Commit Validator (Observer Pattern)
3
+ *
4
+ * Handles commit validation logic and API communication
5
+ * Extends Observable to notify observers of validation progress
6
+ */
7
+
8
+ class CommitValidator extends Observable {
9
+ constructor(apiFacade = null) {
10
+ super();
11
+ this.api = apiFacade || new APIFacade();
12
+ this.validationResults = null;
13
+ this.ignoredCommits = new Set();
14
+ // Use commit validation strategy by default (matches original e-pick behavior)
15
+ this.filterContext = new RepositoryFilterContext(new CommitValidationStrategy());
16
+ }
17
+
18
+ /**
19
+ * Extract commits with repo information from parsed data
20
+ * @param {array} rows - Data rows
21
+ * @param {number} commitColumnIndex - Index of commit column
22
+ * @param {number} repoColumnIndex - Index of repo column (optional)
23
+ * @returns {object} Object with commits array and repoData map
24
+ */
25
+ extractCommits(rows, commitColumnIndex, repoColumnIndex = null) {
26
+ const commits = [];
27
+ const repoData = new Map(); // Map commit hash to repo name
28
+
29
+ rows.forEach(row => {
30
+ const commit = row[commitColumnIndex];
31
+ const repo = repoColumnIndex !== null ? row[repoColumnIndex] : null;
32
+
33
+ if (commit != null && commit !== '') {
34
+ const trimmedCommit = String(commit).trim();
35
+ const trimmedRepo = (repo != null && repo !== '') ? String(repo).trim() : null;
36
+
37
+ commits.push(trimmedCommit);
38
+ if (trimmedRepo && trimmedRepo !== 'undefined' && trimmedRepo !== 'null') {
39
+ repoData.set(trimmedCommit, trimmedRepo);
40
+ }
41
+ }
42
+ });
43
+
44
+ // Remove duplicates while preserving repo mapping
45
+ const uniqueCommits = [...new Set(commits)];
46
+
47
+ return {
48
+ commits: uniqueCommits,
49
+ repoData
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Filter commits by repository using configured strategy
55
+ * @param {array} commits - Array of commit hashes
56
+ * @param {Map} repoData - Map of commit to repo name
57
+ * @returns {Promise<object>} Filtered commits and statistics
58
+ */
59
+ async filterByRepo(commits, repoData) {
60
+ return await this.filterContext.executeFilter(commits, repoData);
61
+ }
62
+
63
+ /**
64
+ * Set the filtering strategy
65
+ * @param {BaseFilterStrategy} strategy - Strategy to use for filtering
66
+ */
67
+ setFilterStrategy(strategy) {
68
+ this.filterContext.setStrategy(strategy);
69
+ }
70
+
71
+ /**
72
+ * Validate commits via API
73
+ * @param {array} commits - Array of commit hashes
74
+ * @param {object} filterStats - Repository filtering statistics (optional)
75
+ * @returns {Promise<object>} Validation results
76
+ */
77
+ async validateCommits(commits, filterStats = null) {
78
+ try {
79
+ // Notify observers that validation has started
80
+ this.notify({
81
+ type: 'start',
82
+ total: commits.length,
83
+ message: 'Starting commit validation...'
84
+ });
85
+
86
+ const result = await this.api.validateCommits(commits);
87
+
88
+ // Add filter stats to results
89
+ if (filterStats) {
90
+ result.filterStats = filterStats;
91
+ }
92
+
93
+ this.validationResults = result;
94
+
95
+ // Notify observers that validation is complete
96
+ this.notify({
97
+ type: 'complete',
98
+ total: commits.length,
99
+ valid: result.summary.valid,
100
+ invalid: result.summary.invalid,
101
+ message: `Validation complete: ${result.summary.valid} valid, ${result.summary.invalid} invalid`
102
+ });
103
+
104
+ return result;
105
+ } catch (error) {
106
+ // Notify observers of error
107
+ this.notify({
108
+ type: 'error',
109
+ message: error.message
110
+ });
111
+
112
+ throw new Error(`Failed to validate commits: ${error.message}`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Toggle ignore status for a commit
118
+ * @param {string} commitHash - Commit hash to toggle
119
+ */
120
+ toggleIgnore(commitHash) {
121
+ if (this.ignoredCommits.has(commitHash)) {
122
+ this.ignoredCommits.delete(commitHash);
123
+ } else {
124
+ this.ignoredCommits.add(commitHash);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Get list of ignored commits
130
+ * @returns {array} Array of ignored commit hashes
131
+ */
132
+ getIgnoredCommits() {
133
+ return Array.from(this.ignoredCommits);
134
+ }
135
+
136
+ /**
137
+ * Get valid commits (excluding ignored)
138
+ * @returns {array} Array of valid commit hashes
139
+ */
140
+ getValidCommits() {
141
+ if (!this.validationResults) {
142
+ return [];
143
+ }
144
+
145
+ return this.validationResults.results
146
+ .filter(r => r.isValid && !this.ignoredCommits.has(r.commit))
147
+ .map(r => r.commit);
148
+ }
149
+
150
+ /**
151
+ * Generate cherry-pick command
152
+ * @param {object} options - Cherry-pick options
153
+ * @returns {Promise<object>} Generated command
154
+ */
155
+ async generateCommand(options = {}) {
156
+ const validCommits = this.getValidCommits();
157
+
158
+ if (validCommits.length === 0) {
159
+ throw new Error('No valid commits to cherry-pick');
160
+ }
161
+
162
+ try {
163
+ return await this.api.generateCommand(
164
+ validCommits,
165
+ this.getIgnoredCommits(),
166
+ options
167
+ );
168
+ } catch (error) {
169
+ throw new Error(`Failed to generate command: ${error.message}`);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Clear all validation data
175
+ */
176
+ clear() {
177
+ this.validationResults = null;
178
+ this.ignoredCommits.clear();
179
+ }
180
+ }
181
+
182
+ // Export for use in other scripts
183
+ window.CommitValidator = CommitValidator;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * File Parser (Factory Pattern + Template Method Pattern)
3
+ *
4
+ * Provides a simple API for parsing Excel and CSV files.
5
+ * Delegates to ParserFactory which uses Template Method pattern internally.
6
+ */
7
+
8
+ class FileParser {
9
+ /**
10
+ * Parse file using appropriate parser
11
+ * @param {File} file - File to parse
12
+ * @returns {Promise<object>} Parsed result
13
+ */
14
+ static async parseFile(file) {
15
+ return await ParserFactory.parseFile(file);
16
+ }
17
+
18
+ /**
19
+ * Parse specific sheet from Excel file
20
+ * @param {ExcelParser} parser - Excel parser instance
21
+ * @param {string} sheetName - Sheet name to parse
22
+ * @returns {object} Parsed sheet data
23
+ */
24
+ static parseSheet(parser, sheetName) {
25
+ return ParserFactory.parseSheet(parser, sheetName);
26
+ }
27
+ }
28
+
29
+ // Export for use in other scripts
30
+ window.FileParser = FileParser;
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Repository Filter Strategy Pattern
3
+ *
4
+ * Provides different strategies for filtering commits by repository
5
+ */
6
+
7
+ /**
8
+ * Base Filter Strategy
9
+ */
10
+ class BaseFilterStrategy {
11
+ /**
12
+ * Filter commits by repository
13
+ * @param {array} commits - Array of commit hashes
14
+ * @param {Map} repoData - Map of commit to repo name
15
+ * @returns {Promise<object>} Filtered commits and statistics
16
+ */
17
+ async filter(commits, repoData) {
18
+ throw new Error('filter() must be implemented by subclass');
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Commit Validation Strategy
24
+ * Matches repository by validating if commits exist in current repo
25
+ * (Similar to original e-pick getRepoName logic)
26
+ */
27
+ class CommitValidationStrategy extends BaseFilterStrategy {
28
+ constructor(apiFacade = null) {
29
+ super();
30
+ this.api = apiFacade || new APIFacade();
31
+ }
32
+
33
+ async filter(commits, repoData) {
34
+ if (!repoData || repoData.size === 0) {
35
+ return {
36
+ filtered: commits,
37
+ stats: {
38
+ total: commits.length,
39
+ matched: commits.length,
40
+ filtered: 0,
41
+ matchedByCommit: false,
42
+ strategy: 'no-filter'
43
+ }
44
+ };
45
+ }
46
+
47
+ // Get unique repo names from data (filter out null/undefined/non-strings)
48
+ const repoNames = [...new Set(repoData.values())].filter(name =>
49
+ name != null && name !== '' && typeof name === 'string'
50
+ );
51
+
52
+ if (repoNames.length === 0) {
53
+ return {
54
+ filtered: commits,
55
+ stats: {
56
+ total: commits.length,
57
+ matched: commits.length,
58
+ filtered: 0,
59
+ matchedByCommit: false,
60
+ strategy: 'no-valid-repos'
61
+ }
62
+ };
63
+ }
64
+
65
+ // Find which repo actually has commits in current repository
66
+ let matchedRepo = null;
67
+
68
+ for (const repoName of repoNames) {
69
+ // Find a commit from this repo
70
+ const sampleCommit = commits.find(commit => repoData.get(commit) === repoName);
71
+
72
+ if (sampleCommit) {
73
+ // Check if this commit exists in current repository
74
+ try {
75
+ const data = await this.api.checkCommit(sampleCommit);
76
+
77
+ if (data.exists) {
78
+ matchedRepo = repoName;
79
+ break;
80
+ }
81
+ } catch (error) {
82
+ console.error(`Error checking commit ${sampleCommit}:`, error);
83
+ }
84
+ }
85
+ }
86
+
87
+ if (!matchedRepo) {
88
+ return {
89
+ filtered: commits,
90
+ stats: {
91
+ total: commits.length,
92
+ matched: commits.length,
93
+ filtered: 0,
94
+ availableRepos: repoNames,
95
+ matchedByCommit: false,
96
+ strategy: 'commit-validation',
97
+ warning: `No repository matched by commit validation. Using all commits from: ${repoNames.join(', ')}`
98
+ }
99
+ };
100
+ }
101
+
102
+ // Filter commits by matched repo
103
+ const filtered = commits.filter(commit =>
104
+ repoData.get(commit) === matchedRepo
105
+ );
106
+
107
+ return {
108
+ filtered,
109
+ stats: {
110
+ total: commits.length,
111
+ matched: filtered.length,
112
+ filtered: commits.length - filtered.length,
113
+ matchedRepo,
114
+ matchedByCommit: true,
115
+ strategy: 'commit-validation',
116
+ availableRepos: repoNames
117
+ }
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Name Matching Strategy
124
+ * Simple case-insensitive name matching (fallback strategy)
125
+ */
126
+ class NameMatchingStrategy extends BaseFilterStrategy {
127
+ constructor(currentRepoName) {
128
+ super();
129
+ this.currentRepoName = currentRepoName;
130
+ }
131
+
132
+ async filter(commits, repoData) {
133
+ if (!repoData || repoData.size === 0 || !this.currentRepoName) {
134
+ return {
135
+ filtered: commits,
136
+ stats: {
137
+ total: commits.length,
138
+ matched: commits.length,
139
+ filtered: 0,
140
+ matchedByCommit: false,
141
+ strategy: 'no-filter'
142
+ }
143
+ };
144
+ }
145
+
146
+ const repoNames = [...new Set(repoData.values())].filter(name =>
147
+ name != null && name !== '' && typeof name === 'string'
148
+ );
149
+
150
+ if (repoNames.length === 0) {
151
+ return {
152
+ filtered: commits,
153
+ stats: {
154
+ total: commits.length,
155
+ matched: commits.length,
156
+ filtered: 0,
157
+ matchedByCommit: false,
158
+ strategy: 'name-matching'
159
+ }
160
+ };
161
+ }
162
+
163
+ // Find matching repo (case-insensitive)
164
+ const matchedRepo = repoNames.find(name =>
165
+ name && this.currentRepoName &&
166
+ typeof name === 'string' && typeof this.currentRepoName === 'string' &&
167
+ name.toLowerCase() === this.currentRepoName.toLowerCase()
168
+ );
169
+
170
+ if (!matchedRepo) {
171
+ return {
172
+ filtered: commits,
173
+ stats: {
174
+ total: commits.length,
175
+ matched: commits.length,
176
+ filtered: 0,
177
+ availableRepos: repoNames,
178
+ matchedByCommit: false,
179
+ strategy: 'name-matching',
180
+ warning: `No exact match for "${this.currentRepoName}". Using all commits.`
181
+ }
182
+ };
183
+ }
184
+
185
+ const filtered = commits.filter(commit =>
186
+ repoData.get(commit) === matchedRepo
187
+ );
188
+
189
+ return {
190
+ filtered,
191
+ stats: {
192
+ total: commits.length,
193
+ matched: filtered.length,
194
+ filtered: commits.length - filtered.length,
195
+ matchedRepo,
196
+ matchedByCommit: false,
197
+ strategy: 'name-matching',
198
+ availableRepos: repoNames
199
+ }
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Filter Context
206
+ * Manages the filter strategy
207
+ */
208
+ class RepositoryFilterContext {
209
+ constructor(strategy) {
210
+ this.strategy = strategy;
211
+ }
212
+
213
+ setStrategy(strategy) {
214
+ this.strategy = strategy;
215
+ }
216
+
217
+ async executeFilter(commits, repoData) {
218
+ return await this.strategy.filter(commits, repoData);
219
+ }
220
+ }
221
+
222
+ // Export for use in other scripts
223
+ window.CommitValidationStrategy = CommitValidationStrategy;
224
+ window.NameMatchingStrategy = NameMatchingStrategy;
225
+ window.RepositoryFilterContext = RepositoryFilterContext;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Observable (Observer Pattern)
3
+ *
4
+ * Allows objects to subscribe to and receive notifications about events
5
+ */
6
+
7
+ class Observable {
8
+ constructor() {
9
+ this.observers = [];
10
+ }
11
+
12
+ /**
13
+ * Subscribe an observer
14
+ * @param {object} observer - Observer with update() method
15
+ * @returns {function} Unsubscribe function
16
+ */
17
+ subscribe(observer) {
18
+ if (typeof observer.update !== 'function') {
19
+ throw new Error('Observer must have an update() method');
20
+ }
21
+
22
+ this.observers.push(observer);
23
+
24
+ // Return unsubscribe function
25
+ return () => {
26
+ this.unsubscribe(observer);
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Unsubscribe an observer
32
+ * @param {object} observer - Observer to remove
33
+ */
34
+ unsubscribe(observer) {
35
+ const index = this.observers.indexOf(observer);
36
+ if (index > -1) {
37
+ this.observers.splice(index, 1);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Notify all observers
43
+ * @param {any} data - Data to send to observers
44
+ */
45
+ notify(data) {
46
+ this.observers.forEach(observer => {
47
+ try {
48
+ observer.update(data);
49
+ } catch (error) {
50
+ console.error('Observer update failed:', error);
51
+ }
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Clear all observers
57
+ */
58
+ clearObservers() {
59
+ this.observers = [];
60
+ }
61
+
62
+ /**
63
+ * Get observer count
64
+ * @returns {number} Number of observers
65
+ */
66
+ getObserverCount() {
67
+ return this.observers.length;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Progress Observer
73
+ * Specialized observer for tracking progress events
74
+ */
75
+ class ProgressObserver {
76
+ constructor(callbacks = {}) {
77
+ this.callbacks = {
78
+ onStart: callbacks.onStart || (() => {}),
79
+ onProgress: callbacks.onProgress || (() => {}),
80
+ onComplete: callbacks.onComplete || (() => {}),
81
+ onError: callbacks.onError || (() => {})
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Update method called by observable
87
+ * @param {object} data - Event data
88
+ */
89
+ update(data) {
90
+ const { type } = data;
91
+
92
+ switch (type) {
93
+ case 'start':
94
+ this.callbacks.onStart(data);
95
+ break;
96
+ case 'progress':
97
+ this.callbacks.onProgress(data);
98
+ break;
99
+ case 'complete':
100
+ this.callbacks.onComplete(data);
101
+ break;
102
+ case 'error':
103
+ this.callbacks.onError(data);
104
+ break;
105
+ default:
106
+ console.warn('Unknown event type:', type);
107
+ }
108
+ }
109
+ }
110
+
111
+ // Export for use in other scripts
112
+ window.Observable = Observable;
113
+ window.ProgressObserver = ProgressObserver;