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.
- package/CLAUDE.md +339 -0
- package/README.md +109 -0
- package/package.json +38 -8
- package/public/css/styles.css +994 -0
- package/public/index.html +226 -0
- 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 -81
- package/lib/pick-commit.js +0 -162
|
@@ -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;
|