@elsikora/git-branch-lint 1.1.0 → 1.1.1-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/README.md +74 -15
- package/bin/application/use-cases/check-working-directory-use-case.d.ts +13 -0
- package/bin/application/use-cases/check-working-directory.use-case.d.ts +13 -0
- package/bin/application/use-cases/create-branch-use-case.d.ts +14 -0
- package/bin/application/use-cases/create-branch.use-case.d.ts +14 -0
- package/bin/application/use-cases/get-branch-config.use-case.d.ts +15 -0
- package/bin/application/use-cases/{get-current-branch-use-case.d.ts → get-current-branch.use-case.d.ts} +1 -1
- package/bin/application/use-cases/{lint-branch-name-use-case.d.ts → lint-branch-name.use-case.d.ts} +4 -4
- package/bin/application/use-cases/push-branch.use-case.d.ts +13 -0
- package/bin/application/use-cases/validate-branch-name-use-case.d.ts +15 -0
- package/bin/application/use-cases/validate-branch-name.use-case.d.ts +15 -0
- package/bin/domain/errors/branch-creation.error.d.ts +24 -0
- package/bin/domain/interface/branch.repository.interface.d.ts +23 -0
- package/bin/domain/{interfaces/repositories/iconfig-repository.d.ts → interface/config.repository.interface.d.ts} +2 -2
- package/bin/domain/interface/repository.interface.d.ts +2 -0
- package/bin/domain/type/branch.type.d.ts +5 -0
- package/bin/domain/type/config.type.d.ts +7 -0
- package/bin/domain/{interfaces → type}/rules.type.d.ts +1 -1
- package/bin/index.d.ts +1 -1
- package/bin/index.js +432 -188
- package/bin/index.js.map +1 -1
- package/bin/infrastructure/config/{cosmiconfig-repository.d.ts → cosmiconfig.repository.d.ts} +4 -4
- package/bin/infrastructure/git/git-branch.repository.d.ts +26 -0
- package/bin/presentation/cli/controllers/create-branch.controller.d.ts +25 -0
- package/bin/presentation/cli/{cli-controller.d.ts → controllers/lint.controller.d.ts} +5 -5
- package/bin/presentation/cli/formatters/branch-choice.formatter.d.ts +27 -0
- package/bin/presentation/cli/formatters/{hint-formatter.d.ts → hint.formatter.d.ts} +2 -2
- package/bin/presentation/cli/prompts/branch-creation.prompt.d.ts +25 -0
- package/package.json +25 -18
- package/bin/application/create-branch-tool/createBranch.d.ts +0 -1
- package/bin/application/create-branch-tool/utils/alignChoices.d.ts +0 -8
- package/bin/application/create-branch-tool/utils/branchNameQuiz.d.ts +0 -2
- package/bin/application/create-branch-tool/utils/runGitCommand.d.ts +0 -1
- package/bin/application/use-cases/get-branch-config-use-case.d.ts +0 -19
- package/bin/domain/interfaces/branch.type.d.ts +0 -5
- package/bin/domain/interfaces/config.type.d.ts +0 -7
- package/bin/domain/interfaces/repositories/ibranch-repository.d.ts +0 -9
- package/bin/domain/interfaces/repository-interfaces.d.ts +0 -2
- package/bin/domain/repositories/branch-repository.d.ts +0 -10
- package/bin/domain/repositories/config-repository.d.ts +0 -12
- package/bin/infrastructure/git/git-branch-repository.d.ts +0 -11
- /package/bin/domain/{entities/branch.d.ts → entity/branch.entity.d.ts} +0 -0
- /package/bin/domain/errors/{lint-errors.d.ts → lint.error.d.ts} +0 -0
- /package/bin/presentation/cli/formatters/{error-formatter.d.ts → error.formatter.d.ts} +0 -0
package/bin/index.js
CHANGED
|
@@ -1,191 +1,111 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import yargs from 'yargs';
|
|
3
|
-
import { execSync, exec } from 'node:child_process';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
3
|
import { cosmiconfig } from 'cosmiconfig';
|
|
4
|
+
import { exec } from 'node:child_process';
|
|
6
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Base error class for branch creation errors
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
support: { description: "🛠️ Support and maintenance tasks", title: "Support" },
|
|
19
|
-
},
|
|
20
|
-
ignore: ["dev"],
|
|
21
|
-
rules: {
|
|
22
|
-
"branch-max-length": 50,
|
|
23
|
-
"branch-min-length": 5,
|
|
24
|
-
"branch-pattern": ":type/:name",
|
|
25
|
-
"branch-prohibited": ["main", "master", "release"],
|
|
26
|
-
"branch-subject-pattern": "[a-z0-9-]+",
|
|
27
|
-
},
|
|
28
|
-
};
|
|
12
|
+
class BranchCreationError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "BranchCreationError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
29
18
|
/**
|
|
30
|
-
*
|
|
19
|
+
* Error thrown when trying to create a branch that already exists
|
|
31
20
|
*/
|
|
32
|
-
class
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* @returns A promise that resolves to the branch configuration
|
|
37
|
-
*/
|
|
38
|
-
async getConfig(appName) {
|
|
39
|
-
const configExplorer = cosmiconfig(appName, {
|
|
40
|
-
packageProp: `elsikora.${appName}`,
|
|
41
|
-
searchPlaces: ["package.json", `.elsikora/.${appName}rc`, `.elsikora/.${appName}rc.json`, `.elsikora/.${appName}rc.yaml`, `.elsikora/.${appName}rc.yml`, `.elsikora/.${appName}rc.js`, `.elsikora/.${appName}rc.ts`, `.elsikora/.${appName}rc.mjs`, `.elsikora/.${appName}rc.cjs`, `.elsikora/${appName}.config.js`, `.elsikora/${appName}.config.ts`, `.elsikora/${appName}.config.mjs`, `.elsikora/${appName}.config.cjs`],
|
|
42
|
-
});
|
|
43
|
-
const result = await configExplorer.search();
|
|
44
|
-
if (!result || result.isEmpty) {
|
|
45
|
-
return DEFAULT_CONFIG;
|
|
46
|
-
}
|
|
47
|
-
// Convert the config to match our interfaces
|
|
48
|
-
const providedConfig = result.config;
|
|
49
|
-
const mergedConfig = {
|
|
50
|
-
...DEFAULT_CONFIG,
|
|
51
|
-
...providedConfig,
|
|
52
|
-
};
|
|
53
|
-
return mergedConfig;
|
|
21
|
+
class BranchAlreadyExistsError extends BranchCreationError {
|
|
22
|
+
constructor(branchName) {
|
|
23
|
+
super(`You are already on branch ${branchName}!`);
|
|
24
|
+
this.name = "BranchAlreadyExistsError";
|
|
54
25
|
}
|
|
55
26
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
short: branchName,
|
|
65
|
-
value: branchName,
|
|
66
|
-
};
|
|
67
|
-
});
|
|
27
|
+
/**
|
|
28
|
+
* Error thrown when git operation fails
|
|
29
|
+
*/
|
|
30
|
+
class GitOperationError extends BranchCreationError {
|
|
31
|
+
constructor(operation, details) {
|
|
32
|
+
const message = details ? `Git operation failed: ${operation} - ${details}` : `Git operation failed: ${operation}`;
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "GitOperationError";
|
|
68
35
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
short: branchList[branchName].title,
|
|
78
|
-
value: branchName,
|
|
79
|
-
};
|
|
80
|
-
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Error thrown when working directory has uncommitted changes
|
|
39
|
+
*/
|
|
40
|
+
class UncommittedChangesError extends BranchCreationError {
|
|
41
|
+
constructor() {
|
|
42
|
+
super("You have uncommitted changes. Please commit or stash them before creating a new branch.");
|
|
43
|
+
this.name = "UncommittedChangesError";
|
|
81
44
|
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const branchNameQuiz = async (branchList) => {
|
|
85
|
-
const { branchType } = await inquirer.prompt([
|
|
86
|
-
{
|
|
87
|
-
choices: alignChoices(branchList),
|
|
88
|
-
message: "Select the type of branch you're creating:",
|
|
89
|
-
name: "branchType",
|
|
90
|
-
type: "list",
|
|
91
|
-
},
|
|
92
|
-
]);
|
|
93
|
-
const { branchName } = await inquirer.prompt([
|
|
94
|
-
{
|
|
95
|
-
message: "Enter the branch name (e.g., authorization):",
|
|
96
|
-
name: "branchName",
|
|
97
|
-
type: "input",
|
|
98
|
-
// Валидация названия ветки
|
|
99
|
-
// eslint-disable-next-line @elsikora/sonar/function-return-type
|
|
100
|
-
validate: (input) => {
|
|
101
|
-
if (!input.trim()) {
|
|
102
|
-
return "Branch name cannot be empty!";
|
|
103
|
-
}
|
|
104
|
-
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
105
|
-
return "Branch name can only contain lowercase letters, numbers, and hyphens!";
|
|
106
|
-
}
|
|
107
|
-
return true;
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
]);
|
|
111
|
-
const fullBranchName = `${branchType}/${branchName}`;
|
|
112
|
-
console.log(`\n⌛️ Creating branch: ${fullBranchName}`);
|
|
113
|
-
return fullBranchName;
|
|
114
|
-
};
|
|
45
|
+
}
|
|
115
46
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
catch (error) {
|
|
124
|
-
console.error(`❌ Error executing command: ${command}`);
|
|
125
|
-
console.error(error.message);
|
|
126
|
-
process.exit(1);
|
|
47
|
+
/**
|
|
48
|
+
* Use case for checking working directory status
|
|
49
|
+
*/
|
|
50
|
+
class CheckWorkingDirectoryUseCase {
|
|
51
|
+
branchRepository;
|
|
52
|
+
constructor(branchRepository) {
|
|
53
|
+
this.branchRepository = branchRepository;
|
|
127
54
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// Проверка чистоты рабочей директории (есть ли незакомиченные файлы)
|
|
138
|
-
const status = execSync("git status --porcelain", { encoding: "utf8" });
|
|
139
|
-
if (status) {
|
|
140
|
-
console.log("⚠️ You have uncommitted changes. Please commit or stash them before creating a new branch.");
|
|
141
|
-
process.exit(1);
|
|
55
|
+
/**
|
|
56
|
+
* Execute the use case
|
|
57
|
+
* @throws {UncommittedChangesError} When there are uncommitted changes
|
|
58
|
+
*/
|
|
59
|
+
async execute() {
|
|
60
|
+
const hasChanges = await this.branchRepository.hasUncommittedChanges();
|
|
61
|
+
if (hasChanges) {
|
|
62
|
+
throw new UncommittedChangesError();
|
|
63
|
+
}
|
|
142
64
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
runGitCommand(`git checkout -b ${branchName}`);
|
|
153
|
-
const { shouldPush } = await inquirer.prompt([
|
|
154
|
-
{
|
|
155
|
-
default: false,
|
|
156
|
-
message: "Do you want to push the branch to the remote repository?",
|
|
157
|
-
name: "shouldPush",
|
|
158
|
-
type: "confirm",
|
|
159
|
-
},
|
|
160
|
-
]);
|
|
161
|
-
if (shouldPush) {
|
|
162
|
-
runGitCommand(`git push --set-upstream origin ${branchName}`);
|
|
163
|
-
console.log(`✅ Branch ${branchName} pushed to remote repository!`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Use case for creating a new branch
|
|
69
|
+
*/
|
|
70
|
+
class CreateBranchUseCase {
|
|
71
|
+
branchRepository;
|
|
72
|
+
constructor(branchRepository) {
|
|
73
|
+
this.branchRepository = branchRepository;
|
|
164
74
|
}
|
|
165
|
-
|
|
166
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Execute the use case
|
|
77
|
+
* @param branchName The name of the branch to create
|
|
78
|
+
* @throws {BranchAlreadyExistsError} When trying to create current branch
|
|
79
|
+
*/
|
|
80
|
+
async execute(branchName) {
|
|
81
|
+
const currentBranch = await this.branchRepository.getCurrentBranchName();
|
|
82
|
+
if (currentBranch === branchName) {
|
|
83
|
+
throw new BranchAlreadyExistsError(branchName);
|
|
84
|
+
}
|
|
85
|
+
await this.branchRepository.createBranch(branchName);
|
|
167
86
|
}
|
|
168
|
-
}
|
|
87
|
+
}
|
|
169
88
|
|
|
170
89
|
/**
|
|
171
|
-
* Use case for
|
|
90
|
+
* Use case for retrieving branch configuration
|
|
172
91
|
*/
|
|
173
92
|
class GetBranchConfigUseCase {
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Constructor
|
|
177
|
-
* @param configRepository The configuration repository
|
|
178
|
-
*/
|
|
93
|
+
configRepository;
|
|
179
94
|
constructor(configRepository) {
|
|
180
|
-
this.
|
|
95
|
+
this.configRepository = configRepository;
|
|
181
96
|
}
|
|
182
97
|
/**
|
|
183
98
|
* Execute the use case
|
|
184
|
-
* @param appName The application name
|
|
99
|
+
* @param appName - The application name
|
|
185
100
|
* @returns The branch configuration
|
|
186
101
|
*/
|
|
187
102
|
async execute(appName) {
|
|
188
|
-
|
|
103
|
+
try {
|
|
104
|
+
return await this.configRepository.getConfig(appName);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new Error(`Failed to get branch config: ${error.message}`);
|
|
108
|
+
}
|
|
189
109
|
}
|
|
190
110
|
}
|
|
191
111
|
|
|
@@ -252,7 +172,6 @@ class LintError extends Error {
|
|
|
252
172
|
*/
|
|
253
173
|
class BranchTooLongError extends LintError {
|
|
254
174
|
constructor(branchName, maxLength) {
|
|
255
|
-
// eslint-disable-next-line @elsikora/typescript/restrict-template-expressions
|
|
256
175
|
super(`Branch name "${branchName}" is too long (maximum length: ${maxLength})`);
|
|
257
176
|
this.name = "BranchTooLongError";
|
|
258
177
|
}
|
|
@@ -262,7 +181,6 @@ class BranchTooLongError extends LintError {
|
|
|
262
181
|
*/
|
|
263
182
|
class BranchTooShortError extends LintError {
|
|
264
183
|
constructor(branchName, minLength) {
|
|
265
|
-
// eslint-disable-next-line @elsikora/typescript/restrict-template-expressions
|
|
266
184
|
super(`Branch name "${branchName}" is too short (minimum length: ${minLength})`);
|
|
267
185
|
this.name = "BranchTooShortError";
|
|
268
186
|
}
|
|
@@ -287,11 +205,11 @@ class ProhibitedBranchError extends LintError {
|
|
|
287
205
|
}
|
|
288
206
|
|
|
289
207
|
/**
|
|
290
|
-
* Use case for linting
|
|
208
|
+
* Use case for linting branch names
|
|
291
209
|
*/
|
|
292
210
|
class LintBranchNameUseCase {
|
|
293
211
|
/**
|
|
294
|
-
*
|
|
212
|
+
* Execute the use case
|
|
295
213
|
* @param branchName The branch name to lint
|
|
296
214
|
* @param config The branch configuration
|
|
297
215
|
* @throws {ProhibitedBranchError} When branch name is prohibited
|
|
@@ -301,7 +219,7 @@ class LintBranchNameUseCase {
|
|
|
301
219
|
*/
|
|
302
220
|
execute(branchName, config) {
|
|
303
221
|
const branch = new Branch(branchName);
|
|
304
|
-
const configRules = config.rules;
|
|
222
|
+
const configRules = config.rules ?? {};
|
|
305
223
|
const ignoreList = config.ignore ?? [];
|
|
306
224
|
if (configRules?.["branch-prohibited"] && branch.isProhibited(configRules["branch-prohibited"])) {
|
|
307
225
|
throw new ProhibitedBranchError(branchName);
|
|
@@ -330,8 +248,10 @@ class LintBranchNameUseCase {
|
|
|
330
248
|
if (!branchNamePattern) {
|
|
331
249
|
return;
|
|
332
250
|
}
|
|
251
|
+
// Get branch types - handle both array and object formats
|
|
252
|
+
const branchTypes = Array.isArray(config.branches) ? config.branches : Object.keys(config.branches);
|
|
333
253
|
const parameters = {
|
|
334
|
-
type:
|
|
254
|
+
type: branchTypes,
|
|
335
255
|
// Если branch-name-pattern не определён, не добавляем name в params
|
|
336
256
|
...(subjectNamePattern && { name: [subjectNamePattern] }),
|
|
337
257
|
};
|
|
@@ -364,19 +284,342 @@ class LintBranchNameUseCase {
|
|
|
364
284
|
}
|
|
365
285
|
}
|
|
366
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Use case for pushing a branch to remote repository
|
|
289
|
+
*/
|
|
290
|
+
class PushBranchUseCase {
|
|
291
|
+
branchRepository;
|
|
292
|
+
constructor(branchRepository) {
|
|
293
|
+
this.branchRepository = branchRepository;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Execute the use case
|
|
297
|
+
* @param branchName The name of the branch to push
|
|
298
|
+
*/
|
|
299
|
+
async execute(branchName) {
|
|
300
|
+
await this.branchRepository.pushBranch(branchName);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Default configuration for branch linting
|
|
306
|
+
*/
|
|
307
|
+
const DEFAULT_CONFIG = {
|
|
308
|
+
branches: {
|
|
309
|
+
bugfix: { description: "🐞 Fixing issues in existing functionality", title: "Bugfix" },
|
|
310
|
+
feature: { description: "🆕 Integration of new functionality", title: "Feature" },
|
|
311
|
+
hotfix: { description: "🚑 Critical fixes for urgent issues", title: "Hotfix" },
|
|
312
|
+
release: { description: "📦 Preparing a new release version", title: "Release" },
|
|
313
|
+
support: { description: "🛠️ Support and maintenance tasks", title: "Support" },
|
|
314
|
+
},
|
|
315
|
+
ignore: ["dev"],
|
|
316
|
+
rules: {
|
|
317
|
+
"branch-max-length": 50,
|
|
318
|
+
"branch-min-length": 5,
|
|
319
|
+
"branch-pattern": ":type/:name",
|
|
320
|
+
"branch-prohibited": ["main", "master", "release"],
|
|
321
|
+
"branch-subject-pattern": "[a-z0-9-]+",
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Cosmiconfig implementation of ConfigRepository
|
|
326
|
+
*/
|
|
327
|
+
class CosmiconfigRepository {
|
|
328
|
+
/**
|
|
329
|
+
* Get the branch configuration
|
|
330
|
+
* @param appName The name of the application
|
|
331
|
+
* @returns A promise that resolves to the branch configuration
|
|
332
|
+
*/
|
|
333
|
+
async getConfig(appName) {
|
|
334
|
+
const configExplorer = cosmiconfig(appName, {
|
|
335
|
+
packageProp: `elsikora.${appName}`,
|
|
336
|
+
searchPlaces: ["package.json", `.elsikora/.${appName}rc`, `.elsikora/.${appName}rc.json`, `.elsikora/.${appName}rc.yaml`, `.elsikora/.${appName}rc.yml`, `.elsikora/.${appName}rc.js`, `.elsikora/.${appName}rc.ts`, `.elsikora/.${appName}rc.mjs`, `.elsikora/.${appName}rc.cjs`, `.elsikora/${appName}.config.js`, `.elsikora/${appName}.config.ts`, `.elsikora/${appName}.config.mjs`, `.elsikora/${appName}.config.cjs`],
|
|
337
|
+
});
|
|
338
|
+
const result = await configExplorer.search();
|
|
339
|
+
if (!result || result.isEmpty) {
|
|
340
|
+
return DEFAULT_CONFIG;
|
|
341
|
+
}
|
|
342
|
+
// Convert the config to match our interfaces
|
|
343
|
+
const providedConfig = result.config;
|
|
344
|
+
const mergedConfig = {
|
|
345
|
+
...DEFAULT_CONFIG,
|
|
346
|
+
...providedConfig,
|
|
347
|
+
};
|
|
348
|
+
return mergedConfig;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
367
352
|
const execAsync = promisify(exec);
|
|
368
353
|
/**
|
|
369
354
|
* Git implementation of BranchRepository
|
|
370
355
|
*/
|
|
371
356
|
class GitBranchRepository {
|
|
357
|
+
/**
|
|
358
|
+
* Create a new branch
|
|
359
|
+
* @param branchName The name of the branch to create
|
|
360
|
+
*/
|
|
361
|
+
async createBranch(branchName) {
|
|
362
|
+
try {
|
|
363
|
+
const command = `git checkout -b ${branchName}`;
|
|
364
|
+
await execAsync(command);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
throw new GitOperationError("create branch", error.message);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
372
370
|
/**
|
|
373
371
|
* Get the current branch name
|
|
374
372
|
* @returns A promise that resolves to the current branch name
|
|
375
373
|
*/
|
|
376
374
|
async getCurrentBranchName() {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
375
|
+
try {
|
|
376
|
+
const command = "git rev-parse --abbrev-ref HEAD";
|
|
377
|
+
const { stdout } = await execAsync(command);
|
|
378
|
+
return stdout.trim();
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
throw new GitOperationError("get current branch", error.message);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Check if working directory has uncommitted changes
|
|
386
|
+
* @returns A promise that resolves to true if there are uncommitted changes
|
|
387
|
+
*/
|
|
388
|
+
async hasUncommittedChanges() {
|
|
389
|
+
try {
|
|
390
|
+
const command = "git status --porcelain";
|
|
391
|
+
const { stdout } = await execAsync(command);
|
|
392
|
+
return stdout.trim().length > 0;
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
throw new GitOperationError("check working directory status", error.message);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Push branch to remote repository
|
|
400
|
+
* @param branchName The name of the branch to push
|
|
401
|
+
*/
|
|
402
|
+
async pushBranch(branchName) {
|
|
403
|
+
try {
|
|
404
|
+
const command = `git push --set-upstream origin ${branchName}`;
|
|
405
|
+
await execAsync(command);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
throw new GitOperationError("push branch", error.message);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Use case for validating branch name format
|
|
415
|
+
*/
|
|
416
|
+
class ValidateBranchNameUseCase {
|
|
417
|
+
BRANCH_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
418
|
+
/**
|
|
419
|
+
* Execute the use case
|
|
420
|
+
* @param branchName The branch name to validate
|
|
421
|
+
* @returns Validation result with error message if invalid
|
|
422
|
+
*/
|
|
423
|
+
execute(branchName) {
|
|
424
|
+
if (!branchName.trim()) {
|
|
425
|
+
return {
|
|
426
|
+
errorMessage: "Branch name cannot be empty!",
|
|
427
|
+
isValid: false,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (!this.BRANCH_NAME_PATTERN.test(branchName)) {
|
|
431
|
+
return {
|
|
432
|
+
errorMessage: "Branch name can only contain lowercase letters, numbers, and hyphens!",
|
|
433
|
+
isValid: false,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return { isValid: true };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Formatter for branch choices in CLI prompts
|
|
442
|
+
*/
|
|
443
|
+
class BranchChoiceFormatter {
|
|
444
|
+
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
|
|
445
|
+
EMPTY_SPACING_OFFSET = 5;
|
|
446
|
+
/**
|
|
447
|
+
* Format branch list for CLI selection
|
|
448
|
+
* @param branchList The list of branches to format
|
|
449
|
+
* @returns Formatted choices for inquirer prompt
|
|
450
|
+
*/
|
|
451
|
+
format(branchList) {
|
|
452
|
+
if (Array.isArray(branchList)) {
|
|
453
|
+
return this.formatSimpleList(branchList);
|
|
454
|
+
}
|
|
455
|
+
return this.formatDetailedList(branchList);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Format detailed branch list (object with descriptions)
|
|
459
|
+
*/
|
|
460
|
+
formatDetailedList(branchList) {
|
|
461
|
+
const branchNames = Object.keys(branchList);
|
|
462
|
+
const maxNameLength = Math.max(...branchNames.map((name) => name.length));
|
|
463
|
+
return branchNames.map((branchName) => {
|
|
464
|
+
const branch = branchList[branchName];
|
|
465
|
+
const padding = " ".repeat(maxNameLength - branchName.length + this.EMPTY_SPACING_OFFSET);
|
|
466
|
+
return {
|
|
467
|
+
name: `${branch.title}:${padding}${branch.description}`,
|
|
468
|
+
short: branch.title,
|
|
469
|
+
value: branchName,
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Format simple branch list (array of strings)
|
|
475
|
+
*/
|
|
476
|
+
formatSimpleList(branchList) {
|
|
477
|
+
return branchList.map((branchName) => ({
|
|
478
|
+
name: branchName,
|
|
479
|
+
short: branchName,
|
|
480
|
+
value: branchName,
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Prompt service for branch creation interactions
|
|
487
|
+
*/
|
|
488
|
+
class BranchCreationPrompt {
|
|
489
|
+
BRANCH_CHOICE_FORMATTER;
|
|
490
|
+
VALIDATE_BRANCH_NAME_USE_CASE;
|
|
491
|
+
constructor() {
|
|
492
|
+
this.BRANCH_CHOICE_FORMATTER = new BranchChoiceFormatter();
|
|
493
|
+
this.VALIDATE_BRANCH_NAME_USE_CASE = new ValidateBranchNameUseCase();
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Prompt for branch name
|
|
497
|
+
* @returns Branch name
|
|
498
|
+
*/
|
|
499
|
+
async promptBranchName() {
|
|
500
|
+
const result = await inquirer.prompt([
|
|
501
|
+
{
|
|
502
|
+
message: "Enter the branch name (e.g., authorization):",
|
|
503
|
+
name: "branchName",
|
|
504
|
+
type: "input",
|
|
505
|
+
// eslint-disable-next-line @elsikora/sonar/function-return-type
|
|
506
|
+
validate: (input) => {
|
|
507
|
+
const validation = this.VALIDATE_BRANCH_NAME_USE_CASE.execute(input);
|
|
508
|
+
if (validation.isValid) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
return validation.errorMessage ?? "Invalid branch name";
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
]);
|
|
515
|
+
return result.branchName;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Prompt for branch type selection
|
|
519
|
+
* @param branches Available branch types
|
|
520
|
+
* @returns Selected branch type
|
|
521
|
+
*/
|
|
522
|
+
async promptBranchType(branches) {
|
|
523
|
+
const choices = this.BRANCH_CHOICE_FORMATTER.format(branches);
|
|
524
|
+
const result = await inquirer.prompt([
|
|
525
|
+
{
|
|
526
|
+
choices,
|
|
527
|
+
message: "Select the type of branch you're creating:",
|
|
528
|
+
name: "branchType",
|
|
529
|
+
type: "list",
|
|
530
|
+
},
|
|
531
|
+
]);
|
|
532
|
+
return result.branchType;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Prompt to push branch to remote
|
|
536
|
+
* @returns Whether to push the branch
|
|
537
|
+
*/
|
|
538
|
+
async promptPushBranch() {
|
|
539
|
+
const result = await inquirer.prompt([
|
|
540
|
+
{
|
|
541
|
+
// eslint-disable-next-line @elsikora/typescript/naming-convention
|
|
542
|
+
default: false,
|
|
543
|
+
message: "Do you want to push the branch to the remote repository?",
|
|
544
|
+
name: "shouldPush",
|
|
545
|
+
type: "confirm",
|
|
546
|
+
},
|
|
547
|
+
]);
|
|
548
|
+
return result.shouldPush;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Controller for branch creation CLI operations
|
|
554
|
+
*/
|
|
555
|
+
class CreateBranchController {
|
|
556
|
+
BRANCH_CREATION_PROMPT;
|
|
557
|
+
CHECK_WORKING_DIRECTORY_USE_CASE;
|
|
558
|
+
CREATE_BRANCH_USE_CASE;
|
|
559
|
+
GET_BRANCH_CONFIG_USE_CASE;
|
|
560
|
+
PUSH_BRANCH_USE_CASE;
|
|
561
|
+
constructor(checkWorkingDirectoryUseCase, createBranchUseCase, getBranchConfigUseCase, pushBranchUseCase) {
|
|
562
|
+
this.CHECK_WORKING_DIRECTORY_USE_CASE = checkWorkingDirectoryUseCase;
|
|
563
|
+
this.CREATE_BRANCH_USE_CASE = createBranchUseCase;
|
|
564
|
+
this.GET_BRANCH_CONFIG_USE_CASE = getBranchConfigUseCase;
|
|
565
|
+
this.PUSH_BRANCH_USE_CASE = pushBranchUseCase;
|
|
566
|
+
this.BRANCH_CREATION_PROMPT = new BranchCreationPrompt();
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Execute the branch creation flow
|
|
570
|
+
* @param appName The application name for configuration
|
|
571
|
+
*/
|
|
572
|
+
async execute(appName) {
|
|
573
|
+
try {
|
|
574
|
+
// Check working directory
|
|
575
|
+
await this.CHECK_WORKING_DIRECTORY_USE_CASE.execute();
|
|
576
|
+
console.error("🌿 Creating a new branch...\n");
|
|
577
|
+
// Get configuration
|
|
578
|
+
const config = await this.GET_BRANCH_CONFIG_USE_CASE.execute(appName);
|
|
579
|
+
// Prompt for branch details
|
|
580
|
+
const branchType = await this.BRANCH_CREATION_PROMPT.promptBranchType(config.branches);
|
|
581
|
+
const branchName = await this.BRANCH_CREATION_PROMPT.promptBranchName();
|
|
582
|
+
const fullBranchName = `${branchType}/${branchName}`;
|
|
583
|
+
console.error(`\n⌛️ Creating branch: ${fullBranchName}`);
|
|
584
|
+
// Create the branch
|
|
585
|
+
await this.CREATE_BRANCH_USE_CASE.execute(fullBranchName);
|
|
586
|
+
// Ask about pushing to remote
|
|
587
|
+
const shouldPush = await this.BRANCH_CREATION_PROMPT.promptPushBranch();
|
|
588
|
+
if (shouldPush) {
|
|
589
|
+
await this.PUSH_BRANCH_USE_CASE.execute(fullBranchName);
|
|
590
|
+
console.error(`✅ Branch ${fullBranchName} pushed to remote repository!`);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
console.error(`✅ Branch ${fullBranchName} created locally!`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
this.handleError(error);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Handle errors that occur during branch creation
|
|
602
|
+
* @param error The error that occurred
|
|
603
|
+
*/
|
|
604
|
+
handleError(error) {
|
|
605
|
+
if (error instanceof UncommittedChangesError) {
|
|
606
|
+
console.error(`⚠️ ${error.message}`);
|
|
607
|
+
// eslint-disable-next-line @elsikora/unicorn/no-process-exit
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
if (error instanceof BranchAlreadyExistsError) {
|
|
611
|
+
console.error(`⚠️ ${error.message}`);
|
|
612
|
+
// eslint-disable-next-line @elsikora/unicorn/no-process-exit
|
|
613
|
+
process.exit(0);
|
|
614
|
+
}
|
|
615
|
+
if (error instanceof BranchCreationError) {
|
|
616
|
+
console.error(`❌ ${error.message}`);
|
|
617
|
+
// eslint-disable-next-line @elsikora/unicorn/no-process-exit
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
console.error(`❌ Failed to create branch: ${error.message}`);
|
|
621
|
+
// eslint-disable-next-line @elsikora/unicorn/no-process-exit
|
|
622
|
+
process.exit(1);
|
|
380
623
|
}
|
|
381
624
|
}
|
|
382
625
|
|
|
@@ -440,9 +683,9 @@ class HintFormatter {
|
|
|
440
683
|
}
|
|
441
684
|
|
|
442
685
|
/**
|
|
443
|
-
* Controller for CLI operations
|
|
686
|
+
* Controller for linting CLI operations
|
|
444
687
|
*/
|
|
445
|
-
class
|
|
688
|
+
class LintController {
|
|
446
689
|
ERROR_FORMATTER;
|
|
447
690
|
GET_BRANCH_CONFIG_USE_CASE;
|
|
448
691
|
GET_CURRENT_BRANCH_USE_CASE;
|
|
@@ -488,7 +731,7 @@ class CliController {
|
|
|
488
731
|
// Get the configuration using the service instead of hardcoded values
|
|
489
732
|
const config = await this.GET_BRANCH_CONFIG_USE_CASE.execute("git-branch-lint");
|
|
490
733
|
console.error(this.ERROR_FORMATTER.format(error.message));
|
|
491
|
-
console.
|
|
734
|
+
console.error(this.HINT_FORMATTER.format(error, config));
|
|
492
735
|
// Since this is a CLI tool, it's appropriate to exit the process on validation errors
|
|
493
736
|
// ESLint wants us to throw instead, but this is a CLI application where exit is acceptable
|
|
494
737
|
// eslint-disable-next-line @elsikora/unicorn/no-process-exit
|
|
@@ -509,7 +752,8 @@ class CliController {
|
|
|
509
752
|
* Application name used for configuration
|
|
510
753
|
*/
|
|
511
754
|
const APP_NAME = "git-branch-lint";
|
|
512
|
-
const
|
|
755
|
+
const ARGS_SLICE_INDEX = 2;
|
|
756
|
+
const argv = yargs(process.argv.slice(ARGS_SLICE_INDEX))
|
|
513
757
|
.option("branch", {
|
|
514
758
|
alias: "b",
|
|
515
759
|
description: "Run branch creation tool",
|
|
@@ -522,28 +766,28 @@ const argv = yargs(process.argv.slice(2))
|
|
|
522
766
|
* Main function that bootstraps the application
|
|
523
767
|
*/
|
|
524
768
|
const main = async () => {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
769
|
+
// Infrastructure layer
|
|
770
|
+
const configRepository = new CosmiconfigRepository();
|
|
771
|
+
const branchRepository = new GitBranchRepository();
|
|
772
|
+
// Application layer - common use cases
|
|
773
|
+
const getBranchConfigUseCase = new GetBranchConfigUseCase(configRepository);
|
|
774
|
+
const shouldRunBranch = Boolean(argv.branch);
|
|
775
|
+
if (shouldRunBranch) {
|
|
776
|
+
// Application layer - branch creation use cases
|
|
777
|
+
const checkWorkingDirectoryUseCase = new CheckWorkingDirectoryUseCase(branchRepository);
|
|
778
|
+
const createBranchUseCase = new CreateBranchUseCase(branchRepository);
|
|
779
|
+
const pushBranchUseCase = new PushBranchUseCase(branchRepository);
|
|
780
|
+
// Presentation layer
|
|
781
|
+
const createBranchController = new CreateBranchController(checkWorkingDirectoryUseCase, createBranchUseCase, getBranchConfigUseCase, pushBranchUseCase);
|
|
782
|
+
await createBranchController.execute(APP_NAME);
|
|
534
783
|
}
|
|
535
784
|
else {
|
|
536
|
-
//
|
|
537
|
-
const configRepository = new CosmiconfigRepository();
|
|
538
|
-
const branchRepository = new GitBranchRepository();
|
|
539
|
-
// Application layer
|
|
540
|
-
const getBranchConfigUseCase = new GetBranchConfigUseCase(configRepository);
|
|
785
|
+
// Application layer - linting use cases
|
|
541
786
|
const getCurrentBranchUseCase = new GetCurrentBranchUseCase(branchRepository);
|
|
542
787
|
const lintBranchNameUseCase = new LintBranchNameUseCase();
|
|
543
788
|
// Presentation layer
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
await cliController.execute(APP_NAME);
|
|
789
|
+
const lintController = new LintController(getBranchConfigUseCase, getCurrentBranchUseCase, lintBranchNameUseCase);
|
|
790
|
+
await lintController.execute(APP_NAME);
|
|
547
791
|
}
|
|
548
792
|
};
|
|
549
793
|
// Bootstrap the application and handle errors
|