1

add: import code

портирован код из старого репозитория https://di9.ru/git/Voomra/Conventional-Commits
This commit is contained in:
2025-07-30 18:44:50 +03:00
commit af99bebf91
16 changed files with 575 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
package commitlint
import (
"bufio"
"commitlint/internal/commitlint/configuration"
"commitlint/internal/commitlint/validator"
"fmt"
"os"
)
func EntryPoint(pCommitMessagePath *string, pConfigPath *string) error {
pConfig, err := configuration.CreateConfig(pConfigPath)
if err != nil {
return err
}
commitMessage, err := ReadCommitMessage(*pCommitMessagePath)
if err != nil {
return err
}
pResult := validator.Validate(commitMessage, *pConfig)
if pResult != nil {
var errMessage string
switch pResult.Result {
case validator.IncorrectPattern:
errMessage = "Incorrect commit message format"
case validator.UnknownType:
errMessage = fmt.Sprintf("Unknown commit type '%s'", pResult.UnknownType)
case validator.UnknownContext:
errMessage = fmt.Sprintf("Unknown commit context '%s'", pResult.UnknownContext)
case validator.OverlongLine:
errMessage = fmt.Sprintf("Line number %d exceeds the allowed length", pResult.Line)
case validator.BlankLine:
errMessage = "Use blank line before BODY"
}
_, err := fmt.Fprintln(os.Stderr, errMessage)
return err
}
return nil
}
func ReadCommitMessage(path string) ([]string, error) {
var lines []string
file, err := os.Open(path)
if err != nil {
panic(err)
}
defer func(file *os.File) {
_ = file.Close()
}(file)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
return lines, err
}

View File

@@ -0,0 +1,35 @@
package commitlint
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestReadCommitMessage(t *testing.T) {
createTmpFile := func() string {
tmpFile, err := os.CreateTemp("", "COMMIT_EDITMSG")
if err != nil {
panic(err)
}
defer func(tmpFile *os.File) {
_ = tmpFile.Close()
}(tmpFile)
_, err = tmpFile.WriteString("Line 1\nLine 2\nLine 3")
if err != nil {
panic(err)
}
return tmpFile.Name()
}
exceptedContent := []string{"Line 1", "Line 2", "Line 3"}
message, err := ReadCommitMessage(createTmpFile())
if err != nil {
panic(err)
}
assert.Equal(t, exceptedContent, message)
}

View File

@@ -0,0 +1,105 @@
package configuration
import (
"encoding/json"
"io"
"os"
"regexp"
)
type UserConfig struct {
Types *[]string `json:"types"`
Contexts *[]string `json:"contexts"`
Excludes *[]string `json:"excludes"`
MaxLengthLine *int `json:"maxLengthLine"`
}
type Config struct {
Types []string
Contexts []string
Excludes []*regexp.Regexp
MaxLengthLine int
}
func CreateConfig(pPath *string) (*Config, error) {
config := Config{
Types: []string{"fix", "feat"},
Contexts: []string{},
Excludes: []*regexp.Regexp{},
MaxLengthLine: 72,
}
if pPath != nil && isFileExists(*pPath) {
userConfig, err := readUserConfig(*pPath)
if err != nil {
return nil, err
}
mergeConfigs(&config, userConfig)
}
return &config, nil
}
func isFileExists(path string) bool {
info, err := os.Stat(path)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
func readUserConfig(path string) (*UserConfig, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer func(fileJson *os.File) {
err := fileJson.Close()
if err != nil {
panic(err)
}
}(file)
bytes, err := io.ReadAll(file)
if err != nil {
return nil, err
}
var userConfig UserConfig
err = json.Unmarshal(bytes, &userConfig)
if err != nil {
return nil, err
}
return &userConfig, nil
}
func mergeConfigs(config *Config, userConfig *UserConfig) {
if userConfig.Types != nil {
for _, item := range *(*userConfig).Types {
config.Types = append(config.Types, item)
}
}
if userConfig.Contexts != nil {
for _, item := range *(userConfig.Contexts) {
config.Contexts = append(config.Contexts, item)
}
}
if userConfig.Excludes != nil {
for _, item := range *(userConfig.Excludes) {
pattern, err := regexp.Compile("(?i)" + item)
if err != nil {
panic(err)
}
config.Excludes = append(config.Excludes, pattern)
}
}
if userConfig.MaxLengthLine != nil {
config.MaxLengthLine = *(userConfig.MaxLengthLine)
}
}

View File

@@ -0,0 +1,69 @@
package configuration
import (
"github.com/stretchr/testify/assert"
"os"
"regexp"
"testing"
)
func TestReadUserConfig(t *testing.T) {
createTmpJson := func() string {
tmpFile, err := os.CreateTemp("", "config.json")
if err != nil {
panic(err)
}
defer func(tmpFile *os.File) {
_ = tmpFile.Close()
}(tmpFile)
_, err = tmpFile.WriteString(`{"types": ["type1", "type2"], "contexts": ["ctx1", "ctx2"], "excludes": ["^wip$"], "maxLengthLine": 14}`)
if err != nil {
panic(err)
}
return tmpFile.Name()
}
expectedUserConfig := UserConfig{}
expectedUserConfig.Types = &([]string{"type1", "type2"})
expectedUserConfig.Contexts = &([]string{"ctx1", "ctx2"})
expectedUserConfig.Excludes = &([]string{"^wip$"})
expectedUserConfig.MaxLengthLine = new(int)
*(expectedUserConfig.MaxLengthLine) = 14
userConfig, err := readUserConfig(createTmpJson())
if err != nil {
panic(err)
}
assert.Equal(t, expectedUserConfig, *userConfig)
}
func TestMergeConfigs(t *testing.T) {
config := Config{
Types: []string{"fix", "feat"},
Contexts: []string{},
Excludes: []*regexp.Regexp{},
MaxLengthLine: 72,
}
userConfig := UserConfig{}
userConfig.Types = &([]string{"type1", "type2"})
userConfig.Contexts = &([]string{"ctx1", "ctx2"})
userConfig.Excludes = &([]string{"^wip$"})
userConfig.MaxLengthLine = new(int)
*(userConfig.MaxLengthLine) = 14
expectedConfig := Config{
Types: []string{"fix", "feat", "type1", "type2"},
Contexts: []string{"ctx1", "ctx2"},
Excludes: []*regexp.Regexp{},
MaxLengthLine: 14,
}
expectedConfig.Excludes = append(expectedConfig.Excludes, regexp.MustCompile("^wip$"))
mergeConfigs(&config, &userConfig)
assert.Equal(t, expectedConfig, config)
}

View File

@@ -0,0 +1,90 @@
package validator
import (
"commitlint/internal/commitlint/configuration"
"regexp"
"slices"
)
type Result int
const (
IncorrectPattern Result = iota
UnknownType
UnknownContext
OverlongLine
BlankLine
)
type ValidateError struct {
// Тип проблемы
Result Result
// Проблемная строка в коммите. Начинается с 1
Line int
// Переданный не известный тип
UnknownType string
// Переданный не известный контекст
UnknownContext string
}
func Validate(commitMessage []string, config configuration.Config) *ValidateError {
if pResult := validateFirstLine(commitMessage[0], config); pResult != nil {
return pResult
}
if len(commitMessage) > 1 {
if pResult := validateBody(commitMessage[1:], config); pResult != nil {
return pResult
}
}
return nil
}
func validateFirstLine(line string, config configuration.Config) *ValidateError {
for _, pExclude := range config.Excludes {
if pExclude.MatchString(line) {
return nil
}
}
commitFirstLine := regexp.MustCompile("^([a-z0-9]+?)!?(?:\\(([a-z0-9]+?)\\))?: .+$")
match := commitFirstLine.FindAllStringSubmatch(line, -1)
if len(match) == 0 {
return &ValidateError{Result: IncorrectPattern, Line: 1}
}
commitType := match[0][1]
if !slices.Contains(config.Types, commitType) {
return &ValidateError{Result: UnknownType, Line: 1, UnknownType: commitType}
}
commitContext := match[0][2]
if len(commitContext) > 0 && !slices.Contains(config.Contexts, commitContext) {
return &ValidateError{Result: UnknownContext, Line: 1, UnknownContext: commitContext}
}
if len(line) > config.MaxLengthLine {
return &ValidateError{Result: OverlongLine, Line: 1}
}
return nil
}
func validateBody(body []string, config configuration.Config) *ValidateError {
if len(body[0]) > 0 {
return &ValidateError{Result: BlankLine, Line: 2}
}
for i, line := range body[1:] {
if len(line) > config.MaxLengthLine {
return &ValidateError{Result: OverlongLine, Line: i + 3}
}
}
return nil
}

View File

@@ -0,0 +1,83 @@
package validator
import (
"commitlint/internal/commitlint/configuration"
"github.com/stretchr/testify/assert"
"regexp"
"testing"
)
func TestValidateFirstLine(t *testing.T) {
pConfig, err := configuration.CreateConfig(nil)
if err != nil {
panic(err)
}
pConfig.Contexts = append(pConfig.Contexts, "git")
pConfig.Excludes = append(pConfig.Excludes, regexp.MustCompile("(?i)^wip$"))
testTab := []struct {
line string
excepted *ValidateError
}{
{
line: "incorrect line",
excepted: &ValidateError{Result: IncorrectPattern, Line: 1},
},
{
line: "zed: message",
excepted: &ValidateError{Result: UnknownType, Line: 1, UnknownType: "zed"},
},
{
line: "fix(zed): message",
excepted: &ValidateError{Result: UnknownContext, Line: 1, UnknownContext: "zed"},
},
{
line: "fix: message",
excepted: nil,
},
{
line: "fix(git): message",
excepted: nil,
},
{
line: "WIP",
excepted: nil,
},
}
for _, test := range testTab {
pResult := validateFirstLine(test.line, *pConfig)
assert.Equal(t, test.excepted, pResult)
}
}
func TestValidateBody(t *testing.T) {
pConfig, err := configuration.CreateConfig(nil)
if err != nil {
panic(err)
}
pConfig.MaxLengthLine = 10
testTab := []struct {
lines []string
excepted *ValidateError
}{
{
lines: []string{" ", "Some Body"},
excepted: &ValidateError{Result: BlankLine, Line: 2},
},
{
lines: []string{"", "Some Body 1234567890"},
excepted: &ValidateError{Result: OverlongLine, Line: 3},
},
{
lines: []string{"", "Some Body"},
excepted: nil,
},
}
for _, test := range testTab {
pResult := validateBody(test.lines, *pConfig)
assert.Equal(t, test.excepted, pResult)
}
}