@elsikora/git-branch-lint 1.2.1-dev.1 ā 1.2.2-dev.1
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/LICENSE.md +3 -15
- package/README.md +131 -241
- package/bin/application/type/branch-placeholder-definition.type.d.ts +7 -0
- package/bin/application/type/build-branch-name-parameters.type.d.ts +4 -0
- package/bin/application/type/get-branch-placeholder-definitions-parameters.type.d.ts +4 -0
- package/bin/application/type/validate-branch-placeholder-value-parameters.type.d.ts +6 -0
- package/bin/application/use-cases/build-branch-name.use-case.d.ts +10 -0
- package/bin/application/use-cases/get-branch-pattern.use-case.d.ts +8 -0
- package/bin/application/use-cases/get-branch-placeholder-definitions.use-case.d.ts +11 -0
- package/bin/application/use-cases/get-current-branch.use-case.d.ts +1 -5
- package/bin/application/use-cases/lint-branch-name.use-case.d.ts +3 -9
- package/bin/application/use-cases/normalize-ticket-id.use-case.d.ts +9 -0
- package/bin/application/use-cases/validate-branch-placeholder-value.use-case.d.ts +8 -0
- package/bin/domain/config/default-branch-lint-config.d.ts +2 -0
- package/bin/domain/constant/branch-pattern.constant.d.ts +1 -0
- package/bin/domain/constant/ticket-id.constant.d.ts +3 -0
- package/bin/domain/errors/lint.error.d.ts +6 -0
- package/bin/domain/policy/branch-template.policy.d.ts +18 -0
- package/bin/domain/policy/ticket-id.policy.d.ts +9 -0
- package/bin/domain/type/branch-subject-pattern.type.d.ts +1 -0
- package/bin/domain/type/input-validation-result.type.d.ts +4 -0
- package/bin/domain/type/rules.type.d.ts +2 -1
- package/bin/index.js +340 -166
- package/bin/index.js.map +1 -1
- package/bin/infrastructure/config/cosmiconfig.repository.d.ts +0 -4
- package/bin/presentation/cli/controllers/create-branch.controller.d.ts +11 -1
- package/bin/presentation/cli/controllers/lint.controller.d.ts +1 -0
- package/bin/presentation/cli/formatters/branch-choice.formatter.d.ts +1 -6
- package/bin/presentation/cli/prompts/branch-creation.prompt.d.ts +8 -11
- package/bin/presentation/cli/type/branch-choice-formatted.type.d.ts +5 -0
- package/bin/presentation/cli/type/branch-placeholder-prompt-options.type.d.ts +6 -0
- package/bin/presentation/cli/type/branch-type-answer.type.d.ts +3 -0
- package/bin/presentation/cli/type/placeholder-answer.type.d.ts +3 -0
- package/bin/presentation/cli/type/push-branch-answer.type.d.ts +3 -0
- package/package.json +28 -28
- package/bin/application/use-cases/check-working-directory-use-case.d.ts +0 -13
- package/bin/application/use-cases/create-branch-use-case.d.ts +0 -14
- package/bin/application/use-cases/validate-branch-name-use-case.d.ts +0 -15
- package/bin/application/use-cases/validate-branch-name.use-case.d.ts +0 -15
package/bin/index.js
CHANGED
|
@@ -6,6 +6,126 @@ import { promisify } from 'node:util';
|
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
|
|
9
|
+
const DEFAULT_BRANCH_SUBJECT_PATTERN_SOURCE = "[a-z0-9-]+";
|
|
10
|
+
|
|
11
|
+
const TICKET_ID_EXAMPLE = "PROJ-123";
|
|
12
|
+
const TICKET_ID_PATTERN_SOURCE = "[a-z]{2,}-[0-9]+";
|
|
13
|
+
|
|
14
|
+
const OPTIONAL_PLACEHOLDER_SUFFIXES = ["-"];
|
|
15
|
+
/**
|
|
16
|
+
* Domain policy for working with branch templates and placeholders.
|
|
17
|
+
*/
|
|
18
|
+
class BranchTemplatePolicy {
|
|
19
|
+
static PLACEHOLDER_PATTERN = /:([a-z](?:[a-z0-9-]*[a-z0-9])?)/gi;
|
|
20
|
+
buildBranchName(branchPattern, placeholderValues) {
|
|
21
|
+
let resolvedBranchName = branchPattern;
|
|
22
|
+
for (const placeholderName of this.getPlaceholders(branchPattern)) {
|
|
23
|
+
const placeholderToken = `:${placeholderName}`;
|
|
24
|
+
const rawValue = placeholderValues[placeholderName] ?? "";
|
|
25
|
+
const normalizedValue = rawValue.trim();
|
|
26
|
+
if (normalizedValue.length === 0 && this.isPlaceholderOptional(branchPattern, placeholderName)) {
|
|
27
|
+
resolvedBranchName = this.removeOptionalPlaceholder(resolvedBranchName, placeholderName);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
resolvedBranchName = resolvedBranchName.replaceAll(placeholderToken, normalizedValue);
|
|
31
|
+
}
|
|
32
|
+
return this.normalizeBranchNameDelimiters(resolvedBranchName);
|
|
33
|
+
}
|
|
34
|
+
buildValidationPatterns(branchPattern) {
|
|
35
|
+
let patternVariants = [branchPattern];
|
|
36
|
+
for (const placeholderName of this.getPlaceholders(branchPattern)) {
|
|
37
|
+
if (!this.isPlaceholderOptional(branchPattern, placeholderName)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
patternVariants = patternVariants.flatMap((variant) => [variant, this.removeOptionalPlaceholder(variant, placeholderName)]);
|
|
41
|
+
}
|
|
42
|
+
return [...new Set(patternVariants.map((variant) => this.normalizeBranchNameDelimiters(variant)).filter((variant) => variant.length > 0))];
|
|
43
|
+
}
|
|
44
|
+
getPlaceholders(branchPattern) {
|
|
45
|
+
const matches = branchPattern.matchAll(BranchTemplatePolicy.PLACEHOLDER_PATTERN);
|
|
46
|
+
const orderedPlaceholders = [];
|
|
47
|
+
for (const match of matches) {
|
|
48
|
+
const placeholderName = match[1];
|
|
49
|
+
if (!orderedPlaceholders.includes(placeholderName)) {
|
|
50
|
+
orderedPlaceholders.push(placeholderName);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return orderedPlaceholders;
|
|
54
|
+
}
|
|
55
|
+
isPlaceholderOptional(branchPattern, placeholderName) {
|
|
56
|
+
const placeholderToken = `:${placeholderName}`;
|
|
57
|
+
return OPTIONAL_PLACEHOLDER_SUFFIXES.some((suffix) => branchPattern.includes(`${placeholderToken}${suffix}`));
|
|
58
|
+
}
|
|
59
|
+
resolvePlaceholderPatternSource(placeholderName, branchTypes, subjectPattern) {
|
|
60
|
+
if (placeholderName === "type") {
|
|
61
|
+
return this.createAlternationPattern(branchTypes);
|
|
62
|
+
}
|
|
63
|
+
if (typeof subjectPattern === "object" && subjectPattern?.[placeholderName]) {
|
|
64
|
+
return subjectPattern[placeholderName];
|
|
65
|
+
}
|
|
66
|
+
if (placeholderName === "ticket") {
|
|
67
|
+
return TICKET_ID_PATTERN_SOURCE;
|
|
68
|
+
}
|
|
69
|
+
if (typeof subjectPattern === "string") {
|
|
70
|
+
return subjectPattern;
|
|
71
|
+
}
|
|
72
|
+
return DEFAULT_BRANCH_SUBJECT_PATTERN_SOURCE;
|
|
73
|
+
}
|
|
74
|
+
createAlternationPattern(branchTypes) {
|
|
75
|
+
if (branchTypes.length === 0) {
|
|
76
|
+
return DEFAULT_BRANCH_SUBJECT_PATTERN_SOURCE;
|
|
77
|
+
}
|
|
78
|
+
return branchTypes.map((branchType) => this.escapeRegex(branchType)).join("|");
|
|
79
|
+
}
|
|
80
|
+
escapeRegex(value) {
|
|
81
|
+
return value.replaceAll(/[\\^$.*+?()[\]{}|/-]/g, String.raw `\$&`);
|
|
82
|
+
}
|
|
83
|
+
isDelimiter(character) {
|
|
84
|
+
return character === "." || character === "-" || character === "/" || character === "_";
|
|
85
|
+
}
|
|
86
|
+
normalizeBranchNameDelimiters(branchName) {
|
|
87
|
+
const compactedBranchName = branchName
|
|
88
|
+
.replaceAll(/\/{2,}/g, "/")
|
|
89
|
+
.replaceAll(/-{2,}/g, "-")
|
|
90
|
+
.replaceAll(/_{2,}/g, "_")
|
|
91
|
+
.replaceAll(/\.{2,}/g, ".");
|
|
92
|
+
return this.trimDelimiterEdges(compactedBranchName);
|
|
93
|
+
}
|
|
94
|
+
removeOptionalPlaceholder(branchPattern, placeholderName) {
|
|
95
|
+
const placeholderToken = `:${placeholderName}`;
|
|
96
|
+
let updatedPattern = branchPattern;
|
|
97
|
+
for (const suffix of OPTIONAL_PLACEHOLDER_SUFFIXES) {
|
|
98
|
+
updatedPattern = updatedPattern.replaceAll(`${placeholderToken}${suffix}`, "");
|
|
99
|
+
}
|
|
100
|
+
updatedPattern = updatedPattern.replaceAll(placeholderToken, "");
|
|
101
|
+
return updatedPattern;
|
|
102
|
+
}
|
|
103
|
+
trimDelimiterEdges(value) {
|
|
104
|
+
let startIndex = 0;
|
|
105
|
+
let endIndex = value.length - 1;
|
|
106
|
+
while (startIndex <= endIndex && this.isDelimiter(value[startIndex])) {
|
|
107
|
+
startIndex++;
|
|
108
|
+
}
|
|
109
|
+
while (endIndex >= startIndex && this.isDelimiter(value[endIndex])) {
|
|
110
|
+
endIndex--;
|
|
111
|
+
}
|
|
112
|
+
return value.slice(startIndex, endIndex + 1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Use case for assembling a branch name from validated user inputs.
|
|
118
|
+
*/
|
|
119
|
+
class BuildBranchNameUseCase {
|
|
120
|
+
branchTemplatePolicy;
|
|
121
|
+
constructor(branchTemplatePolicy = new BranchTemplatePolicy()) {
|
|
122
|
+
this.branchTemplatePolicy = branchTemplatePolicy;
|
|
123
|
+
}
|
|
124
|
+
execute(parameters) {
|
|
125
|
+
return this.branchTemplatePolicy.buildBranchName(parameters.branchPattern, parameters.placeholderValues);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
9
129
|
/**
|
|
10
130
|
* Base error class for branch creation errors
|
|
11
131
|
*/
|
|
@@ -109,24 +229,65 @@ class GetBranchConfigUseCase {
|
|
|
109
229
|
}
|
|
110
230
|
}
|
|
111
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Use case for retrieving effective branch pattern.
|
|
234
|
+
*/
|
|
235
|
+
class GetBranchPatternUseCase {
|
|
236
|
+
DEFAULT_BRANCH_PATTERN = ":type/:name";
|
|
237
|
+
execute(config) {
|
|
238
|
+
return config.rules?.["branch-pattern"] ?? this.DEFAULT_BRANCH_PATTERN;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Use case for resolving placeholder definitions from branch pattern config.
|
|
244
|
+
*/
|
|
245
|
+
class GetBranchPlaceholderDefinitionsUseCase {
|
|
246
|
+
branchTemplatePolicy;
|
|
247
|
+
constructor(branchTemplatePolicy = new BranchTemplatePolicy()) {
|
|
248
|
+
this.branchTemplatePolicy = branchTemplatePolicy;
|
|
249
|
+
}
|
|
250
|
+
execute(parameters) {
|
|
251
|
+
const branchPattern = parameters.config.rules?.["branch-pattern"];
|
|
252
|
+
const subjectPattern = parameters.config.rules?.["branch-subject-pattern"];
|
|
253
|
+
if (!branchPattern) {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
const branchTypes = Array.isArray(parameters.config.branches) ? parameters.config.branches : Object.keys(parameters.config.branches);
|
|
257
|
+
const placeholderNames = this.branchTemplatePolicy.getPlaceholders(branchPattern);
|
|
258
|
+
return placeholderNames.map((placeholderName) => {
|
|
259
|
+
if (placeholderName === "type") {
|
|
260
|
+
return {
|
|
261
|
+
isOptional: false,
|
|
262
|
+
isTypePlaceholder: true,
|
|
263
|
+
placeholderName,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
...(placeholderName === "ticket" && { example: TICKET_ID_EXAMPLE }),
|
|
268
|
+
isOptional: this.branchTemplatePolicy.isPlaceholderOptional(branchPattern, placeholderName),
|
|
269
|
+
isTypePlaceholder: false,
|
|
270
|
+
patternSource: this.branchTemplatePolicy.resolvePlaceholderPatternSource(placeholderName, branchTypes, subjectPattern),
|
|
271
|
+
placeholderName,
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
112
277
|
/**
|
|
113
278
|
* Use case for getting the current branch name
|
|
114
279
|
*/
|
|
115
280
|
class GetCurrentBranchUseCase {
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Constructor
|
|
119
|
-
* @param branchRepository The branch repository
|
|
120
|
-
*/
|
|
281
|
+
branchRepository;
|
|
121
282
|
constructor(branchRepository) {
|
|
122
|
-
this.
|
|
283
|
+
this.branchRepository = branchRepository;
|
|
123
284
|
}
|
|
124
285
|
/**
|
|
125
286
|
* Execute the use case
|
|
126
287
|
* @returns The current branch name
|
|
127
288
|
*/
|
|
128
289
|
async execute() {
|
|
129
|
-
return this.
|
|
290
|
+
return this.branchRepository.getCurrentBranchName();
|
|
130
291
|
}
|
|
131
292
|
}
|
|
132
293
|
|
|
@@ -185,6 +346,15 @@ class BranchTooShortError extends LintError {
|
|
|
185
346
|
this.name = "BranchTooShortError";
|
|
186
347
|
}
|
|
187
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Error thrown when a branch placeholder pattern in config is invalid.
|
|
351
|
+
*/
|
|
352
|
+
class InvalidBranchPatternConfigError extends LintError {
|
|
353
|
+
constructor(placeholderName, patternSource) {
|
|
354
|
+
super(`Invalid branch pattern config for "${placeholderName}": ${patternSource}`);
|
|
355
|
+
this.name = "InvalidBranchPatternConfigError";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
188
358
|
/**
|
|
189
359
|
* Error thrown when branch name doesn't match the pattern
|
|
190
360
|
*/
|
|
@@ -208,6 +378,10 @@ class ProhibitedBranchError extends LintError {
|
|
|
208
378
|
* Use case for linting branch names
|
|
209
379
|
*/
|
|
210
380
|
class LintBranchNameUseCase {
|
|
381
|
+
BRANCH_TEMPLATE_POLICY;
|
|
382
|
+
constructor(branchTemplatePolicy = new BranchTemplatePolicy()) {
|
|
383
|
+
this.BRANCH_TEMPLATE_POLICY = branchTemplatePolicy;
|
|
384
|
+
}
|
|
211
385
|
/**
|
|
212
386
|
* Execute the use case
|
|
213
387
|
* @param branchName The branch name to lint
|
|
@@ -235,48 +409,6 @@ class LintBranchNameUseCase {
|
|
|
235
409
|
}
|
|
236
410
|
this.validatePattern(branch.getName(), config);
|
|
237
411
|
}
|
|
238
|
-
/**
|
|
239
|
-
* Test a branch name against a specific pattern
|
|
240
|
-
* @param branchName The branch name to test
|
|
241
|
-
* @param pattern The pattern to test against
|
|
242
|
-
* @param branchTypes Available branch types
|
|
243
|
-
* @param subjectNamePattern Pattern for the name/description part
|
|
244
|
-
* @returns true if pattern matches
|
|
245
|
-
*/
|
|
246
|
-
testPattern(branchName, pattern, branchTypes, subjectNamePattern) {
|
|
247
|
-
let processedPattern = pattern;
|
|
248
|
-
const parameters = {
|
|
249
|
-
type: branchTypes,
|
|
250
|
-
// Add ticket pattern if present (accepts lowercase letters)
|
|
251
|
-
...(processedPattern.includes(":ticket") && { ticket: ["[a-z]{2,}-[0-9]+"] }),
|
|
252
|
-
// Add name pattern if specified
|
|
253
|
-
...(subjectNamePattern && { name: [subjectNamePattern] }),
|
|
254
|
-
};
|
|
255
|
-
// Process parameters
|
|
256
|
-
for (const [key, values] of Object.entries(parameters)) {
|
|
257
|
-
const placeholder = `:${key.toLowerCase()}`;
|
|
258
|
-
let replacement = "(";
|
|
259
|
-
for (let index = 0; index < values.length; index++) {
|
|
260
|
-
const value = values[index];
|
|
261
|
-
if (value.startsWith("[")) {
|
|
262
|
-
replacement += value;
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
const escapedValue = value.replaceAll(/[-/\\^$*+?.()|[\]{}]/g, String.raw `\$&`);
|
|
266
|
-
replacement += escapedValue;
|
|
267
|
-
}
|
|
268
|
-
if (index < values.length - 1) {
|
|
269
|
-
replacement += "|";
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
replacement += ")";
|
|
273
|
-
processedPattern = processedPattern.replaceAll(new RegExp(placeholder, "g"), replacement);
|
|
274
|
-
}
|
|
275
|
-
// Create the regular expression
|
|
276
|
-
const regexp = new RegExp(`^${processedPattern}$`);
|
|
277
|
-
// Test the branch name against the pattern
|
|
278
|
-
return regexp.test(branchName);
|
|
279
|
-
}
|
|
280
412
|
/**
|
|
281
413
|
* Validate the branch name against the pattern
|
|
282
414
|
* @param branchName The branch name to validate
|
|
@@ -286,36 +418,65 @@ class LintBranchNameUseCase {
|
|
|
286
418
|
validatePattern(branchName, config) {
|
|
287
419
|
// Start with original pattern
|
|
288
420
|
const branchNamePattern = config.rules?.["branch-pattern"];
|
|
289
|
-
const
|
|
421
|
+
const subjectPattern = config.rules?.["branch-subject-pattern"];
|
|
290
422
|
if (!branchNamePattern) {
|
|
291
423
|
return;
|
|
292
424
|
}
|
|
293
|
-
// Get branch types - handle both array and object formats
|
|
294
425
|
const branchTypes = Array.isArray(config.branches) ? config.branches : Object.keys(config.branches);
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (this.testPattern(branchName, patternToTry, branchTypes, subjectNamePattern)) {
|
|
311
|
-
return; // Pattern matched, validation passed
|
|
426
|
+
const placeholders = this.BRANCH_TEMPLATE_POLICY.getPlaceholders(branchNamePattern);
|
|
427
|
+
const patternVariants = this.BRANCH_TEMPLATE_POLICY.buildValidationPatterns(branchNamePattern);
|
|
428
|
+
for (const patternVariant of patternVariants) {
|
|
429
|
+
let resolvedPattern = patternVariant;
|
|
430
|
+
for (const placeholder of placeholders) {
|
|
431
|
+
const placeholderToken = `:${placeholder}`;
|
|
432
|
+
if (!resolvedPattern.includes(placeholderToken)) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const patternSource = this.BRANCH_TEMPLATE_POLICY.resolvePlaceholderPatternSource(placeholder, branchTypes, subjectPattern);
|
|
436
|
+
resolvedPattern = resolvedPattern.replaceAll(placeholderToken, `(${patternSource})`);
|
|
437
|
+
}
|
|
438
|
+
const expression = new RegExp(`^${resolvedPattern}$`);
|
|
439
|
+
if (expression.test(branchName)) {
|
|
440
|
+
return;
|
|
312
441
|
}
|
|
313
442
|
}
|
|
314
|
-
// No pattern matched
|
|
315
443
|
throw new PatternMatchError(branchName);
|
|
316
444
|
}
|
|
317
445
|
}
|
|
318
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Domain policy for ticket identifier normalization and validation.
|
|
449
|
+
*/
|
|
450
|
+
class TicketIdPolicy {
|
|
451
|
+
static TICKET_ID_PATTERN = new RegExp(`^${TICKET_ID_PATTERN_SOURCE}$`);
|
|
452
|
+
isEmpty(candidate) {
|
|
453
|
+
return candidate.trim().length === 0;
|
|
454
|
+
}
|
|
455
|
+
isValid(candidate) {
|
|
456
|
+
const normalizedCandidate = this.normalize(candidate);
|
|
457
|
+
if (normalizedCandidate.length === 0) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
return TicketIdPolicy.TICKET_ID_PATTERN.test(normalizedCandidate);
|
|
461
|
+
}
|
|
462
|
+
normalize(candidate) {
|
|
463
|
+
return candidate.trim().toLowerCase();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Use case for normalizing optional ticket identifier input.
|
|
469
|
+
*/
|
|
470
|
+
class NormalizeTicketIdUseCase {
|
|
471
|
+
ticketIdPolicy;
|
|
472
|
+
constructor(ticketIdPolicy = new TicketIdPolicy()) {
|
|
473
|
+
this.ticketIdPolicy = ticketIdPolicy;
|
|
474
|
+
}
|
|
475
|
+
execute(ticketId) {
|
|
476
|
+
return this.ticketIdPolicy.normalize(ticketId);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
319
480
|
/**
|
|
320
481
|
* Use case for pushing a branch to remote repository
|
|
321
482
|
*/
|
|
@@ -335,10 +496,7 @@ class PushBranchUseCase {
|
|
|
335
496
|
|
|
336
497
|
const BRANCH_MAX_LENGTH = 50;
|
|
337
498
|
const BRANCH_MIN_LENGTH = 5;
|
|
338
|
-
|
|
339
|
-
* Default configuration for branch linting
|
|
340
|
-
*/
|
|
341
|
-
const DEFAULT_CONFIG = {
|
|
499
|
+
const DEFAULT_BRANCH_LINT_CONFIG = {
|
|
342
500
|
branches: {
|
|
343
501
|
bugfix: { description: "š Fixing issues in existing functionality", title: "Bugfix" },
|
|
344
502
|
feature: { description: "š Integration of new functionality", title: "Feature" },
|
|
@@ -355,6 +513,7 @@ const DEFAULT_CONFIG = {
|
|
|
355
513
|
"branch-subject-pattern": "[a-z0-9-]+",
|
|
356
514
|
},
|
|
357
515
|
};
|
|
516
|
+
|
|
358
517
|
/**
|
|
359
518
|
* Cosmiconfig implementation of ConfigRepository
|
|
360
519
|
*/
|
|
@@ -371,13 +530,19 @@ class CosmiconfigRepository {
|
|
|
371
530
|
});
|
|
372
531
|
const result = await configExplorer.search();
|
|
373
532
|
if (!result || result.isEmpty) {
|
|
374
|
-
return
|
|
533
|
+
return DEFAULT_BRANCH_LINT_CONFIG;
|
|
375
534
|
}
|
|
376
535
|
// Convert the config to match our interfaces
|
|
377
536
|
const providedConfig = result.config;
|
|
378
537
|
const mergedConfig = {
|
|
379
|
-
...
|
|
538
|
+
...DEFAULT_BRANCH_LINT_CONFIG,
|
|
380
539
|
...providedConfig,
|
|
540
|
+
branches: providedConfig.branches ?? DEFAULT_BRANCH_LINT_CONFIG.branches,
|
|
541
|
+
ignore: providedConfig.ignore ?? DEFAULT_BRANCH_LINT_CONFIG.ignore,
|
|
542
|
+
rules: {
|
|
543
|
+
...DEFAULT_BRANCH_LINT_CONFIG.rules,
|
|
544
|
+
...providedConfig.rules,
|
|
545
|
+
},
|
|
381
546
|
};
|
|
382
547
|
return mergedConfig;
|
|
383
548
|
}
|
|
@@ -445,25 +610,25 @@ class GitBranchRepository {
|
|
|
445
610
|
}
|
|
446
611
|
|
|
447
612
|
/**
|
|
448
|
-
* Use case for validating
|
|
613
|
+
* Use case for validating interactive placeholder values.
|
|
449
614
|
*/
|
|
450
|
-
class
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
errorMessage: "Branch name cannot be empty!",
|
|
461
|
-
isValid: false,
|
|
462
|
-
};
|
|
615
|
+
class ValidateBranchPlaceholderValueUseCase {
|
|
616
|
+
execute(parameters) {
|
|
617
|
+
const normalizedValue = parameters.value.trim();
|
|
618
|
+
if (parameters.isOptional && normalizedValue.length === 0) {
|
|
619
|
+
return { isValid: true };
|
|
620
|
+
}
|
|
621
|
+
let expression;
|
|
622
|
+
try {
|
|
623
|
+
const flags = parameters.placeholderName === "ticket" ? "i" : "";
|
|
624
|
+
expression = new RegExp(`^${parameters.patternSource}$`, flags);
|
|
463
625
|
}
|
|
464
|
-
|
|
626
|
+
catch {
|
|
627
|
+
throw new InvalidBranchPatternConfigError(parameters.placeholderName, parameters.patternSource);
|
|
628
|
+
}
|
|
629
|
+
if (!expression.test(normalizedValue)) {
|
|
465
630
|
return {
|
|
466
|
-
errorMessage:
|
|
631
|
+
errorMessage: `Invalid ${parameters.placeholderName} format`,
|
|
467
632
|
isValid: false,
|
|
468
633
|
};
|
|
469
634
|
}
|
|
@@ -521,31 +686,10 @@ class BranchChoiceFormatter {
|
|
|
521
686
|
*/
|
|
522
687
|
class BranchCreationPrompt {
|
|
523
688
|
BRANCH_CHOICE_FORMATTER;
|
|
524
|
-
|
|
689
|
+
VALIDATE_BRANCH_PLACEHOLDER_VALUE_USE_CASE;
|
|
525
690
|
constructor() {
|
|
526
691
|
this.BRANCH_CHOICE_FORMATTER = new BranchChoiceFormatter();
|
|
527
|
-
this.
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Prompt for branch name
|
|
531
|
-
* @returns Branch name
|
|
532
|
-
*/
|
|
533
|
-
async promptBranchName() {
|
|
534
|
-
const result = await inquirer.prompt([
|
|
535
|
-
{
|
|
536
|
-
message: "Enter the branch name (e.g., authorization):",
|
|
537
|
-
name: "branchName",
|
|
538
|
-
type: "input",
|
|
539
|
-
validate: (input) => {
|
|
540
|
-
const validation = this.VALIDATE_BRANCH_NAME_USE_CASE.execute(input);
|
|
541
|
-
if (validation.isValid) {
|
|
542
|
-
return true;
|
|
543
|
-
}
|
|
544
|
-
return validation.errorMessage ?? "Invalid branch name";
|
|
545
|
-
},
|
|
546
|
-
},
|
|
547
|
-
]);
|
|
548
|
-
return result.branchName;
|
|
692
|
+
this.VALIDATE_BRANCH_PLACEHOLDER_VALUE_USE_CASE = new ValidateBranchPlaceholderValueUseCase();
|
|
549
693
|
}
|
|
550
694
|
/**
|
|
551
695
|
* Prompt for branch type selection
|
|
@@ -564,6 +708,37 @@ class BranchCreationPrompt {
|
|
|
564
708
|
]);
|
|
565
709
|
return result.branchType;
|
|
566
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* Prompt for a branch placeholder value based on a validation pattern.
|
|
713
|
+
*/
|
|
714
|
+
async promptPlaceholder(options) {
|
|
715
|
+
const result = await inquirer.prompt([
|
|
716
|
+
{
|
|
717
|
+
message: this.buildPlaceholderPromptMessage(options),
|
|
718
|
+
name: "value",
|
|
719
|
+
transformer: (input) => {
|
|
720
|
+
if (options.isOptional && input.trim() === "") {
|
|
721
|
+
return "\u001B[2m(Enter to skip)\u001B[0m";
|
|
722
|
+
}
|
|
723
|
+
return input;
|
|
724
|
+
},
|
|
725
|
+
type: "input",
|
|
726
|
+
validate: (input) => {
|
|
727
|
+
const validation = this.VALIDATE_BRANCH_PLACEHOLDER_VALUE_USE_CASE.execute({
|
|
728
|
+
isOptional: options.isOptional,
|
|
729
|
+
patternSource: options.patternSource,
|
|
730
|
+
placeholderName: options.placeholderName,
|
|
731
|
+
value: input,
|
|
732
|
+
});
|
|
733
|
+
if (validation.isValid) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
return validation.errorMessage ?? this.buildPlaceholderValidationMessage(options);
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
]);
|
|
740
|
+
return result.value.trim();
|
|
741
|
+
}
|
|
567
742
|
/**
|
|
568
743
|
* Prompt to push branch to remote
|
|
569
744
|
* @returns Whether to push the branch
|
|
@@ -580,39 +755,15 @@ class BranchCreationPrompt {
|
|
|
580
755
|
]);
|
|
581
756
|
return result.shouldPush;
|
|
582
757
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
transformer: (input) => {
|
|
593
|
-
// Show placeholder when empty
|
|
594
|
-
return input.trim() === "" ? "\u001B[2m(Enter to skip)\u001B[0m" : input;
|
|
595
|
-
},
|
|
596
|
-
type: "input",
|
|
597
|
-
validate: (input) => {
|
|
598
|
-
// Empty is valid (optional field)
|
|
599
|
-
if (!input || input.trim() === "") {
|
|
600
|
-
return true;
|
|
601
|
-
}
|
|
602
|
-
// Normalize input for validation (case-insensitive)
|
|
603
|
-
const normalizedInput = input.trim();
|
|
604
|
-
// Validate format: 2+ letters (any case), dash, digits
|
|
605
|
-
const ticketPattern = /^[A-Z]{2,}-\d+$/i;
|
|
606
|
-
if (!ticketPattern.test(normalizedInput)) {
|
|
607
|
-
return "Invalid format. Expected format: PROJ-123 (2+ letters, dash, numbers)";
|
|
608
|
-
}
|
|
609
|
-
return true;
|
|
610
|
-
},
|
|
611
|
-
},
|
|
612
|
-
]);
|
|
613
|
-
// Return lowercase version or empty string
|
|
614
|
-
const trimmed = result.ticketId.trim();
|
|
615
|
-
return trimmed ? trimmed.toLowerCase() : "";
|
|
758
|
+
buildPlaceholderPromptMessage(options) {
|
|
759
|
+
const normalizedPlaceholderName = options.placeholderName.replaceAll("-", " ");
|
|
760
|
+
const capitalizedPlaceholderName = normalizedPlaceholderName[0].toUpperCase() + normalizedPlaceholderName.slice(1);
|
|
761
|
+
const optionalPart = options.isOptional ? " optional" : "";
|
|
762
|
+
const examplePart = options.example ? `, e.g., ${options.example}` : "";
|
|
763
|
+
return `${capitalizedPlaceholderName}${optionalPart}${examplePart}:`;
|
|
764
|
+
}
|
|
765
|
+
buildPlaceholderValidationMessage(options) {
|
|
766
|
+
return `Invalid ${options.placeholderName} format`;
|
|
616
767
|
}
|
|
617
768
|
}
|
|
618
769
|
|
|
@@ -621,14 +772,24 @@ class BranchCreationPrompt {
|
|
|
621
772
|
*/
|
|
622
773
|
class CreateBranchController {
|
|
623
774
|
BRANCH_CREATION_PROMPT;
|
|
775
|
+
BUILD_BRANCH_NAME_USE_CASE;
|
|
624
776
|
CHECK_WORKING_DIRECTORY_USE_CASE;
|
|
625
777
|
CREATE_BRANCH_USE_CASE;
|
|
626
778
|
GET_BRANCH_CONFIG_USE_CASE;
|
|
779
|
+
GET_BRANCH_PATTERN_USE_CASE;
|
|
780
|
+
GET_BRANCH_PLACEHOLDER_DEFINITIONS_USE_CASE;
|
|
781
|
+
LINT_BRANCH_NAME_USE_CASE;
|
|
782
|
+
NORMALIZE_TICKET_ID_USE_CASE;
|
|
627
783
|
PUSH_BRANCH_USE_CASE;
|
|
628
|
-
constructor(checkWorkingDirectoryUseCase, createBranchUseCase, getBranchConfigUseCase, pushBranchUseCase) {
|
|
784
|
+
constructor(buildBranchNameUseCase, checkWorkingDirectoryUseCase, createBranchUseCase, getBranchPatternUseCase, getBranchPlaceholderDefinitionsUseCase, getBranchConfigUseCase, lintBranchNameUseCase, normalizeTicketIdUseCase, pushBranchUseCase) {
|
|
785
|
+
this.BUILD_BRANCH_NAME_USE_CASE = buildBranchNameUseCase;
|
|
629
786
|
this.CHECK_WORKING_DIRECTORY_USE_CASE = checkWorkingDirectoryUseCase;
|
|
630
787
|
this.CREATE_BRANCH_USE_CASE = createBranchUseCase;
|
|
631
788
|
this.GET_BRANCH_CONFIG_USE_CASE = getBranchConfigUseCase;
|
|
789
|
+
this.GET_BRANCH_PATTERN_USE_CASE = getBranchPatternUseCase;
|
|
790
|
+
this.GET_BRANCH_PLACEHOLDER_DEFINITIONS_USE_CASE = getBranchPlaceholderDefinitionsUseCase;
|
|
791
|
+
this.LINT_BRANCH_NAME_USE_CASE = lintBranchNameUseCase;
|
|
792
|
+
this.NORMALIZE_TICKET_ID_USE_CASE = normalizeTicketIdUseCase;
|
|
632
793
|
this.PUSH_BRANCH_USE_CASE = pushBranchUseCase;
|
|
633
794
|
this.BRANCH_CREATION_PROMPT = new BranchCreationPrompt();
|
|
634
795
|
}
|
|
@@ -643,20 +804,28 @@ class CreateBranchController {
|
|
|
643
804
|
console.error("šæ Creating a new branch...\n");
|
|
644
805
|
// Get configuration
|
|
645
806
|
const config = await this.GET_BRANCH_CONFIG_USE_CASE.execute(appName);
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
const
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
807
|
+
const branchPattern = this.GET_BRANCH_PATTERN_USE_CASE.execute(config);
|
|
808
|
+
const placeholderDefinitions = this.GET_BRANCH_PLACEHOLDER_DEFINITIONS_USE_CASE.execute({ config });
|
|
809
|
+
const placeholderValues = {};
|
|
810
|
+
for (const placeholderDefinition of placeholderDefinitions) {
|
|
811
|
+
if (placeholderDefinition.isTypePlaceholder) {
|
|
812
|
+
placeholderValues.type = await this.BRANCH_CREATION_PROMPT.promptBranchType(config.branches);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const inputValue = await this.BRANCH_CREATION_PROMPT.promptPlaceholder({
|
|
816
|
+
example: placeholderDefinition.example,
|
|
817
|
+
isOptional: placeholderDefinition.isOptional,
|
|
818
|
+
patternSource: placeholderDefinition.patternSource ?? "",
|
|
819
|
+
placeholderName: placeholderDefinition.placeholderName,
|
|
820
|
+
});
|
|
821
|
+
placeholderValues[placeholderDefinition.placeholderName] = placeholderDefinition.placeholderName === "ticket" ? this.NORMALIZE_TICKET_ID_USE_CASE.execute(inputValue) : inputValue;
|
|
659
822
|
}
|
|
823
|
+
const fullBranchName = this.BUILD_BRANCH_NAME_USE_CASE.execute({
|
|
824
|
+
branchPattern,
|
|
825
|
+
placeholderValues,
|
|
826
|
+
});
|
|
827
|
+
// Re-validate the final assembled branch name with full lint rules before creation.
|
|
828
|
+
this.LINT_BRANCH_NAME_USE_CASE.execute(fullBranchName, config);
|
|
660
829
|
console.error(`\nāļø Creating branch: ${fullBranchName}`);
|
|
661
830
|
// Create the branch
|
|
662
831
|
await this.CREATE_BRANCH_USE_CASE.execute(fullBranchName);
|
|
@@ -791,22 +960,22 @@ class LintController {
|
|
|
791
960
|
this.LINT_BRANCH_NAME_USE_CASE.execute(branchName, config);
|
|
792
961
|
}
|
|
793
962
|
catch (error) {
|
|
794
|
-
await this.handleError(error);
|
|
963
|
+
await this.handleError(error, appName);
|
|
795
964
|
}
|
|
796
965
|
}
|
|
797
966
|
/**
|
|
798
967
|
* Handle errors that occur during execution
|
|
799
968
|
* @param error The error that occurred
|
|
969
|
+
* @param appName The application name
|
|
800
970
|
*/
|
|
801
|
-
async handleError(error) {
|
|
971
|
+
async handleError(error, appName) {
|
|
802
972
|
if (!(error instanceof Error)) {
|
|
803
973
|
console.error(this.ERROR_FORMATTER.format("[LintBranchName] Unhandled error occurred"));
|
|
804
974
|
throw new Error("Unknown error occurred");
|
|
805
975
|
}
|
|
806
976
|
if (error instanceof LintError) {
|
|
807
977
|
try {
|
|
808
|
-
|
|
809
|
-
const config = await this.GET_BRANCH_CONFIG_USE_CASE.execute("git-branch-lint");
|
|
978
|
+
const config = await this.GET_BRANCH_CONFIG_USE_CASE.execute(appName);
|
|
810
979
|
console.error(this.ERROR_FORMATTER.format(error.message));
|
|
811
980
|
console.error(this.HINT_FORMATTER.format(error, config));
|
|
812
981
|
// Since this is a CLI tool, it's appropriate to exit the process on validation errors
|
|
@@ -852,10 +1021,15 @@ const main = async () => {
|
|
|
852
1021
|
if (shouldRunBranch) {
|
|
853
1022
|
// Application layer - branch creation use cases
|
|
854
1023
|
const checkWorkingDirectoryUseCase = new CheckWorkingDirectoryUseCase(branchRepository);
|
|
1024
|
+
const buildBranchNameUseCase = new BuildBranchNameUseCase();
|
|
855
1025
|
const createBranchUseCase = new CreateBranchUseCase(branchRepository);
|
|
1026
|
+
const getBranchPatternUseCase = new GetBranchPatternUseCase();
|
|
1027
|
+
const getBranchPlaceholderDefinitionsUseCase = new GetBranchPlaceholderDefinitionsUseCase();
|
|
1028
|
+
const lintBranchNameUseCase = new LintBranchNameUseCase();
|
|
1029
|
+
const normalizeTicketIdUseCase = new NormalizeTicketIdUseCase();
|
|
856
1030
|
const pushBranchUseCase = new PushBranchUseCase(branchRepository);
|
|
857
1031
|
// Presentation layer
|
|
858
|
-
const createBranchController = new CreateBranchController(checkWorkingDirectoryUseCase, createBranchUseCase, getBranchConfigUseCase, pushBranchUseCase);
|
|
1032
|
+
const createBranchController = new CreateBranchController(buildBranchNameUseCase, checkWorkingDirectoryUseCase, createBranchUseCase, getBranchPatternUseCase, getBranchPlaceholderDefinitionsUseCase, getBranchConfigUseCase, lintBranchNameUseCase, normalizeTicketIdUseCase, pushBranchUseCase);
|
|
859
1033
|
await createBranchController.execute(APP_NAME);
|
|
860
1034
|
}
|
|
861
1035
|
else {
|