@intentsolutionsio/devops-automation-pack 1.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-plugin/plugin.json +28 -0
- package/LICENSE +113 -0
- package/README.md +335 -0
- package/package.json +47 -0
- package/skills/generating-conventional-commits/SKILL.md +86 -0
- package/skills/generating-conventional-commits/assets/README.md +6 -0
- package/skills/generating-conventional-commits/assets/commit_message_template.txt +114 -0
- package/skills/generating-conventional-commits/assets/example_code_diff.txt +64 -0
- package/skills/generating-conventional-commits/references/README.md +4 -0
- package/skills/generating-conventional-commits/scripts/README.md +7 -0
- package/skills/generating-conventional-commits/scripts/generate_commit_message.py +307 -0
- package/skills/generating-conventional-commits/scripts/suggest_commit_type.py +349 -0
- package/skills/generating-conventional-commits/scripts/validate_commit_message.py +138 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Suggest the appropriate commit type based on code changes.
|
|
4
|
+
|
|
5
|
+
This script analyzes code changes and suggests the most appropriate
|
|
6
|
+
commit type from the conventional commits specification (feat, fix,
|
|
7
|
+
docs, style, refactor, perf, test, chore, ci, build, revert).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
import subprocess
|
|
13
|
+
import json
|
|
14
|
+
from typing import Tuple, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_git_diff(staged_only: bool = False, ref: Optional[str] = None) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Get git diff content.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
staged_only: If True, get staged changes only
|
|
23
|
+
ref: Optional reference to compare against (e.g., 'main', 'HEAD~1')
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The git diff output
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
cmd = ['git', 'diff']
|
|
30
|
+
if staged_only:
|
|
31
|
+
cmd.append('--cached')
|
|
32
|
+
if ref:
|
|
33
|
+
cmd.append(ref)
|
|
34
|
+
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
cmd,
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
check=False
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return result.stdout if result.returncode == 0 else ""
|
|
43
|
+
except FileNotFoundError:
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_diff_from_file(filepath: str) -> str:
|
|
48
|
+
"""Read diff from a file."""
|
|
49
|
+
try:
|
|
50
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
51
|
+
return f.read()
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
except IOError as e:
|
|
56
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def analyze_changes(diff_content: str) -> Dict[str, int]:
|
|
61
|
+
"""
|
|
62
|
+
Count different types of changes in the diff.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
diff_content: The git diff content
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dictionary of change type counts
|
|
69
|
+
"""
|
|
70
|
+
counts = {
|
|
71
|
+
'test_additions': 0,
|
|
72
|
+
'test_removals': 0,
|
|
73
|
+
'doc_changes': 0,
|
|
74
|
+
'style_changes': 0,
|
|
75
|
+
'feature_additions': 0,
|
|
76
|
+
'feature_removals': 0,
|
|
77
|
+
'performance_changes': 0,
|
|
78
|
+
'bug_references': 0,
|
|
79
|
+
'refactor_changes': 0,
|
|
80
|
+
'ci_changes': 0,
|
|
81
|
+
'dependency_changes': 0,
|
|
82
|
+
'total_additions': 0,
|
|
83
|
+
'total_removals': 0,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lines = diff_content.split('\n')
|
|
87
|
+
current_file = ''
|
|
88
|
+
|
|
89
|
+
for i, line in enumerate(lines):
|
|
90
|
+
# Track current file
|
|
91
|
+
if line.startswith('diff --git'):
|
|
92
|
+
parts = line.split(' ')
|
|
93
|
+
if len(parts) >= 4:
|
|
94
|
+
current_file = parts[3]
|
|
95
|
+
|
|
96
|
+
# Count additions and removals
|
|
97
|
+
if line.startswith('+') and not line.startswith('+++'):
|
|
98
|
+
counts['total_additions'] += 1
|
|
99
|
+
|
|
100
|
+
# Analyze added lines
|
|
101
|
+
content = line[1:]
|
|
102
|
+
if 'test' in current_file.lower() or 'spec' in current_file.lower():
|
|
103
|
+
counts['test_additions'] += 1
|
|
104
|
+
elif 'package.json' in current_file or 'requirements.txt' in current_file:
|
|
105
|
+
counts['dependency_changes'] += 1
|
|
106
|
+
elif any(x in content.lower() for x in ['def ', 'class ', 'function ', 'const ', 'async ']):
|
|
107
|
+
counts['feature_additions'] += 1
|
|
108
|
+
elif any(x in content.lower() for x in ['optimize', 'performance', 'cache', 'lazy']):
|
|
109
|
+
counts['performance_changes'] += 1
|
|
110
|
+
|
|
111
|
+
elif line.startswith('-') and not line.startswith('---'):
|
|
112
|
+
counts['total_removals'] += 1
|
|
113
|
+
|
|
114
|
+
content = line[1:]
|
|
115
|
+
if 'test' in current_file.lower() or 'spec' in current_file.lower():
|
|
116
|
+
counts['test_removals'] += 1
|
|
117
|
+
elif any(x in content.lower() for x in ['def ', 'class ', 'function ', 'const ']):
|
|
118
|
+
counts['feature_removals'] += 1
|
|
119
|
+
|
|
120
|
+
# Detect specific change patterns
|
|
121
|
+
if any(x in line.lower() for x in ['.md', 'readme', 'docs/', 'documentation']):
|
|
122
|
+
counts['doc_changes'] += 1
|
|
123
|
+
|
|
124
|
+
# Style changes (mostly whitespace/formatting)
|
|
125
|
+
if line.startswith('+') or line.startswith('-'):
|
|
126
|
+
if len(line.lstrip('+-').strip()) < 10: # Short lines
|
|
127
|
+
counts['style_changes'] += 1
|
|
128
|
+
|
|
129
|
+
# Bug/fix references
|
|
130
|
+
if any(x in line.lower() for x in ['fix', 'bug', 'issue', 'closes #', 'fixes #']):
|
|
131
|
+
counts['bug_references'] += 1
|
|
132
|
+
|
|
133
|
+
# CI/CD changes
|
|
134
|
+
if '.github/workflows' in current_file or '.gitlab-ci' in current_file or 'Jenkinsfile' in current_file:
|
|
135
|
+
counts['ci_changes'] += 1
|
|
136
|
+
if 'pipeline' in line.lower() or 'workflow' in line.lower():
|
|
137
|
+
counts['ci_changes'] += 1
|
|
138
|
+
|
|
139
|
+
# Refactoring (renaming, restructuring without functional change)
|
|
140
|
+
if any(x in line.lower() for x in ['rename', 'reorganize', 'refactor', 'restructure']):
|
|
141
|
+
counts['refactor_changes'] += 1
|
|
142
|
+
|
|
143
|
+
return counts
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def suggest_commit_type(diff_content: str) -> Tuple[str, float, str]:
|
|
147
|
+
"""
|
|
148
|
+
Suggest the most appropriate commit type.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
diff_content: The git diff content
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Tuple of (suggested_type, confidence, reasoning)
|
|
155
|
+
"""
|
|
156
|
+
changes = analyze_changes(diff_content)
|
|
157
|
+
|
|
158
|
+
# Scoring system: higher score = more confident
|
|
159
|
+
scores = {
|
|
160
|
+
'feat': 0.0,
|
|
161
|
+
'fix': 0.0,
|
|
162
|
+
'docs': 0.0,
|
|
163
|
+
'style': 0.0,
|
|
164
|
+
'refactor': 0.0,
|
|
165
|
+
'perf': 0.0,
|
|
166
|
+
'test': 0.0,
|
|
167
|
+
'chore': 0.0,
|
|
168
|
+
'ci': 0.0,
|
|
169
|
+
'build': 0.0,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
reasoning = []
|
|
173
|
+
|
|
174
|
+
# Test changes
|
|
175
|
+
if changes['test_additions'] > 0:
|
|
176
|
+
scores['test'] += changes['test_additions'] * 2
|
|
177
|
+
reasoning.append(f"Added {changes['test_additions']} test lines")
|
|
178
|
+
|
|
179
|
+
# Documentation changes
|
|
180
|
+
if changes['doc_changes'] > 0:
|
|
181
|
+
scores['docs'] += changes['doc_changes'] * 1.5
|
|
182
|
+
reasoning.append(f"Modified {changes['doc_changes']} documentation lines")
|
|
183
|
+
|
|
184
|
+
# Bug fixes
|
|
185
|
+
if changes['bug_references'] > 0:
|
|
186
|
+
scores['fix'] += changes['bug_references'] * 3
|
|
187
|
+
reasoning.append(f"Found {changes['bug_references']} bug/fix references")
|
|
188
|
+
|
|
189
|
+
# Performance improvements
|
|
190
|
+
if changes['performance_changes'] > 0:
|
|
191
|
+
scores['perf'] += changes['performance_changes'] * 2
|
|
192
|
+
reasoning.append(f"Found {changes['performance_changes']} performance improvements")
|
|
193
|
+
|
|
194
|
+
# CI/CD changes
|
|
195
|
+
if changes['ci_changes'] > 0:
|
|
196
|
+
scores['ci'] += changes['ci_changes'] * 2
|
|
197
|
+
reasoning.append(f"Modified CI/CD files ({changes['ci_changes']} lines)")
|
|
198
|
+
|
|
199
|
+
# Dependency changes
|
|
200
|
+
if changes['dependency_changes'] > 0:
|
|
201
|
+
scores['build'] += changes['dependency_changes'] * 1.5
|
|
202
|
+
reasoning.append(f"Modified dependencies ({changes['dependency_changes']} changes)")
|
|
203
|
+
|
|
204
|
+
# Feature additions
|
|
205
|
+
if changes['feature_additions'] > changes['feature_removals']:
|
|
206
|
+
feature_ratio = changes['feature_additions'] / max(1, changes['feature_removals'])
|
|
207
|
+
scores['feat'] += feature_ratio * 2
|
|
208
|
+
reasoning.append(f"Added {changes['feature_additions']} feature lines")
|
|
209
|
+
|
|
210
|
+
# Refactoring
|
|
211
|
+
if changes['refactor_changes'] > 0:
|
|
212
|
+
scores['refactor'] += changes['refactor_changes'] * 1.5
|
|
213
|
+
reasoning.append(f"Found {changes['refactor_changes']} refactoring indicators")
|
|
214
|
+
|
|
215
|
+
# Style changes
|
|
216
|
+
if changes['style_changes'] > changes['total_additions'] * 0.3:
|
|
217
|
+
scores['style'] += changes['style_changes']
|
|
218
|
+
reasoning.append(f"Mostly style changes ({changes['style_changes']} lines)")
|
|
219
|
+
|
|
220
|
+
# Fallback: determine based on change volume
|
|
221
|
+
total_changes = changes['total_additions'] + changes['total_removals']
|
|
222
|
+
if total_changes == 0:
|
|
223
|
+
return 'chore', 0.5, "No meaningful changes detected"
|
|
224
|
+
|
|
225
|
+
# If nothing scored highly, use refactor as catch-all
|
|
226
|
+
if max(scores.values()) == 0:
|
|
227
|
+
scores['refactor'] = 1.0
|
|
228
|
+
reasoning.append("General code changes without specific category")
|
|
229
|
+
|
|
230
|
+
# Find the highest scoring type
|
|
231
|
+
suggested = max(scores, key=scores.get)
|
|
232
|
+
confidence = scores[suggested] / max(1, total_changes)
|
|
233
|
+
confidence = min(1.0, confidence) # Cap at 1.0
|
|
234
|
+
|
|
235
|
+
reasoning_str = " | ".join(reasoning) if reasoning else "General code changes"
|
|
236
|
+
|
|
237
|
+
return suggested, confidence, reasoning_str
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def format_output(
|
|
241
|
+
suggested_type: str,
|
|
242
|
+
confidence: float,
|
|
243
|
+
reasoning: str,
|
|
244
|
+
format_type: str = 'text'
|
|
245
|
+
) -> str:
|
|
246
|
+
"""Format the output based on requested format."""
|
|
247
|
+
if format_type == 'json':
|
|
248
|
+
return json.dumps({
|
|
249
|
+
'type': suggested_type,
|
|
250
|
+
'confidence': round(confidence, 2),
|
|
251
|
+
'confidence_percent': f"{confidence * 100:.1f}%",
|
|
252
|
+
'reasoning': reasoning
|
|
253
|
+
}, indent=2)
|
|
254
|
+
else:
|
|
255
|
+
confidence_percent = f"{confidence * 100:.1f}%"
|
|
256
|
+
return (
|
|
257
|
+
f"Suggested commit type: {suggested_type}\n"
|
|
258
|
+
f"Confidence: {confidence_percent}\n"
|
|
259
|
+
f"Reasoning: {reasoning}"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def main():
|
|
264
|
+
"""Main entry point."""
|
|
265
|
+
parser = argparse.ArgumentParser(
|
|
266
|
+
description="Suggest the appropriate commit type based on code changes",
|
|
267
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
268
|
+
epilog="""
|
|
269
|
+
Examples:
|
|
270
|
+
# Suggest type from staged changes
|
|
271
|
+
%(prog)s --staged
|
|
272
|
+
|
|
273
|
+
# Suggest type from unstaged changes
|
|
274
|
+
%(prog)s --unstaged
|
|
275
|
+
|
|
276
|
+
# Suggest type from a diff file
|
|
277
|
+
%(prog)s --file changes.diff
|
|
278
|
+
|
|
279
|
+
# Compare against a specific branch
|
|
280
|
+
%(prog)s --ref main
|
|
281
|
+
|
|
282
|
+
# Output as JSON
|
|
283
|
+
%(prog)s --staged --format json
|
|
284
|
+
|
|
285
|
+
# Verbose output with detailed analysis
|
|
286
|
+
%(prog)s --staged --verbose
|
|
287
|
+
"""
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
input_group = parser.add_mutually_exclusive_group()
|
|
291
|
+
input_group.add_argument(
|
|
292
|
+
'--staged',
|
|
293
|
+
action='store_true',
|
|
294
|
+
help='Analyze staged changes (default)'
|
|
295
|
+
)
|
|
296
|
+
input_group.add_argument(
|
|
297
|
+
'--unstaged',
|
|
298
|
+
action='store_true',
|
|
299
|
+
help='Analyze unstaged changes'
|
|
300
|
+
)
|
|
301
|
+
input_group.add_argument(
|
|
302
|
+
'--file',
|
|
303
|
+
type=str,
|
|
304
|
+
help='Path to diff file to analyze'
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
parser.add_argument(
|
|
308
|
+
'--ref',
|
|
309
|
+
type=str,
|
|
310
|
+
help='Git reference to compare against (e.g., main, HEAD~1)'
|
|
311
|
+
)
|
|
312
|
+
parser.add_argument(
|
|
313
|
+
'--format',
|
|
314
|
+
choices=['text', 'json'],
|
|
315
|
+
default='text',
|
|
316
|
+
help='Output format (default: text)'
|
|
317
|
+
)
|
|
318
|
+
parser.add_argument(
|
|
319
|
+
'-v', '--verbose',
|
|
320
|
+
action='store_true',
|
|
321
|
+
help='Enable verbose output with detailed analysis'
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
args = parser.parse_args()
|
|
325
|
+
|
|
326
|
+
# Get diff content
|
|
327
|
+
if args.file:
|
|
328
|
+
diff_content = get_diff_from_file(args.file)
|
|
329
|
+
elif args.unstaged:
|
|
330
|
+
diff_content = get_git_diff(staged_only=False, ref=args.ref)
|
|
331
|
+
else: # Default to staged
|
|
332
|
+
diff_content = get_git_diff(staged_only=True, ref=args.ref)
|
|
333
|
+
|
|
334
|
+
if not diff_content:
|
|
335
|
+
print("Error: No changes to analyze", file=sys.stderr)
|
|
336
|
+
return 1
|
|
337
|
+
|
|
338
|
+
# Suggest commit type
|
|
339
|
+
suggested_type, confidence, reasoning = suggest_commit_type(diff_content)
|
|
340
|
+
|
|
341
|
+
# Output result
|
|
342
|
+
output = format_output(suggested_type, confidence, reasoning, args.format)
|
|
343
|
+
print(output)
|
|
344
|
+
|
|
345
|
+
return 0
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
if __name__ == '__main__':
|
|
349
|
+
sys.exit(main())
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validate commit messages against the Conventional Commits specification.
|
|
4
|
+
|
|
5
|
+
This script validates that commit messages follow the conventional commits
|
|
6
|
+
standard, which includes type(scope): subject format with proper structure.
|
|
7
|
+
|
|
8
|
+
The conventional commits specification defines:
|
|
9
|
+
- type: feat, fix, docs, style, refactor, perf, test, chore, ci, etc.
|
|
10
|
+
- scope: optional area of the codebase
|
|
11
|
+
- subject: brief description of the change
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
import re
|
|
17
|
+
from typing import Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_conventional_commit(message: str) -> Tuple[bool, str]:
|
|
21
|
+
"""
|
|
22
|
+
Validate a commit message against conventional commits specification.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
message: The commit message to validate
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple of (is_valid, error_message)
|
|
29
|
+
- is_valid: True if message is valid, False otherwise
|
|
30
|
+
- error_message: Description of validation error (empty if valid)
|
|
31
|
+
"""
|
|
32
|
+
if not message or not message.strip():
|
|
33
|
+
return False, "Commit message cannot be empty"
|
|
34
|
+
|
|
35
|
+
message = message.strip()
|
|
36
|
+
lines = message.split('\n')
|
|
37
|
+
subject = lines[0]
|
|
38
|
+
|
|
39
|
+
# Validate subject line format
|
|
40
|
+
# Pattern: type(scope)?: subject or type: subject
|
|
41
|
+
# Where type is required, scope is optional, and subject is required
|
|
42
|
+
pattern = r'^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?!?: .{1,}$'
|
|
43
|
+
|
|
44
|
+
if not re.match(pattern, subject):
|
|
45
|
+
return False, (
|
|
46
|
+
f"Invalid commit message format: '{subject}'\n"
|
|
47
|
+
"Expected format: type(scope): subject\n"
|
|
48
|
+
"Valid types: feat, fix, docs, style, refactor, perf, test, chore, ci, build, revert\n"
|
|
49
|
+
"Example: feat(auth): add login functionality"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Validate subject line length (recommended max 50 chars)
|
|
53
|
+
if len(subject) > 72:
|
|
54
|
+
return False, (
|
|
55
|
+
f"Subject line too long ({len(subject)} chars). "
|
|
56
|
+
"Recommended maximum is 50 characters, hard limit is 72"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Validate subject doesn't end with period
|
|
60
|
+
if subject.endswith('.'):
|
|
61
|
+
return False, "Subject line should not end with a period"
|
|
62
|
+
|
|
63
|
+
# If there are multiple lines, validate blank line after subject
|
|
64
|
+
if len(lines) > 1:
|
|
65
|
+
if lines[1].strip() != '':
|
|
66
|
+
return False, "Expected blank line between subject and body"
|
|
67
|
+
|
|
68
|
+
return True, ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main():
|
|
72
|
+
"""Main entry point for the validation script."""
|
|
73
|
+
parser = argparse.ArgumentParser(
|
|
74
|
+
description="Validate commit messages against the Conventional Commits specification",
|
|
75
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
76
|
+
epilog="""
|
|
77
|
+
Examples:
|
|
78
|
+
# Validate a commit message from a file
|
|
79
|
+
%(prog)s --file commit-msg.txt
|
|
80
|
+
|
|
81
|
+
# Validate a commit message from command line
|
|
82
|
+
%(prog)s --message "feat(api): add new endpoint"
|
|
83
|
+
|
|
84
|
+
# Validate with verbose output
|
|
85
|
+
%(prog)s --message "fix: resolve bug" --verbose
|
|
86
|
+
"""
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Create mutually exclusive group for input source
|
|
90
|
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
|
91
|
+
input_group.add_argument(
|
|
92
|
+
'-m', '--message',
|
|
93
|
+
type=str,
|
|
94
|
+
help='Commit message to validate'
|
|
95
|
+
)
|
|
96
|
+
input_group.add_argument(
|
|
97
|
+
'-f', '--file',
|
|
98
|
+
type=str,
|
|
99
|
+
help='Path to file containing commit message'
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
'-v', '--verbose',
|
|
104
|
+
action='store_true',
|
|
105
|
+
help='Enable verbose output'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
args = parser.parse_args()
|
|
109
|
+
|
|
110
|
+
# Get the commit message
|
|
111
|
+
if args.file:
|
|
112
|
+
try:
|
|
113
|
+
with open(args.file, 'r', encoding='utf-8') as f:
|
|
114
|
+
message = f.read()
|
|
115
|
+
except FileNotFoundError:
|
|
116
|
+
print(f"Error: File not found: {args.file}", file=sys.stderr)
|
|
117
|
+
return 1
|
|
118
|
+
except IOError as e:
|
|
119
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
else:
|
|
122
|
+
message = args.message
|
|
123
|
+
|
|
124
|
+
# Validate the message
|
|
125
|
+
is_valid, error_msg = validate_conventional_commit(message)
|
|
126
|
+
|
|
127
|
+
if is_valid:
|
|
128
|
+
if args.verbose:
|
|
129
|
+
print("✓ Commit message is valid")
|
|
130
|
+
return 0
|
|
131
|
+
else:
|
|
132
|
+
print(f"✗ Validation failed:", file=sys.stderr)
|
|
133
|
+
print(error_msg, file=sys.stderr)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == '__main__':
|
|
138
|
+
sys.exit(main())
|