diff --git a/pkg/commit/commit.go b/pkg/commit/commit.go index 03568d1..6d0b76b 100644 --- a/pkg/commit/commit.go +++ b/pkg/commit/commit.go @@ -3,11 +3,12 @@ package commit type Commit struct { - Type string - Scope string - Description string - Body string - Footers []Footer + Type string + Scope string + Subject string + Body string + Footers []*Footer + IsBreaking bool } type Footer struct { diff --git a/pkg/commit/parser.go b/pkg/commit/parser.go index 551d764..d843a5c 100644 --- a/pkg/commit/parser.go +++ b/pkg/commit/parser.go @@ -1,21 +1,51 @@ package commit -import "bytes" - -const ( - cr = 13 - lf = 10 +import ( + "bytes" + "errors" + "regexp" ) -func paragraphs(input []byte) [][]byte { - cln := bytes.ReplaceAll(input, []byte{cr, lf}, []byte{lf}) - cln = bytes.ReplaceAll(cln, []byte{cr}, []byte{lf}) +const ( + cr = 13 + lf = 10 + crlf = "\r\n" +) - ps := bytes.Split(cln, []byte{lf, lf}) +var rHeader = regexp.MustCompile( + `^([\w\-]*)(?:\(([\w\$\.\/\-\* ]*)\))?(!)?\: (.*)$`, +) - for i, p := range ps { - ps[i] = bytes.Trim(p, "\r\n") +func parseHeader(header []byte) (*Commit, error) { + if bytes.ContainsAny(header, crlf) { + return nil, errors.New("header cannot span multiple lines") } - return ps + result := rHeader.FindSubmatch(header) + if result == nil { + return &Commit{Subject: string(header)}, nil + } + + return &Commit{ + Type: string(result[1]), + Scope: string(result[2]), + Subject: string(result[4]), + IsBreaking: (string(result[3]) == "!"), + }, nil +} + +func paragraphs(input []byte) [][]byte { + paras := bytes.Split(normlizeLinefeeds(input), []byte{lf, lf}) + for i, p := range paras { + paras[i] = bytes.Trim(p, crlf) + } + + return paras +} + +func normlizeLinefeeds(input []byte) []byte { + return bytes.ReplaceAll( + bytes.ReplaceAll(input, []byte{cr, lf}, []byte{lf}), + []byte{cr}, []byte{lf}, + ) } diff --git a/pkg/commit/parser_test.go b/pkg/commit/parser_test.go index a35feb9..fa78e2c 100644 --- a/pkg/commit/parser_test.go +++ b/pkg/commit/parser_test.go @@ -6,6 +6,175 @@ import ( "github.com/stretchr/testify/assert" ) +func Test_parseHeader(t *testing.T) { + type args struct { + header []byte + } + tests := []struct { + name string + args args + want *Commit + errStr string + }{ + { + name: "non-convention commit", + args: args{header: []byte("add user sorting option")}, + want: &Commit{Subject: "add user sorting option"}, + }, + { + name: "type only", + args: args{header: []byte("feat: add user sorting option")}, + want: &Commit{Type: "feat", Subject: "add user sorting option"}, + }, + { + name: "type and scope", + args: args{header: []byte("feat(user): add user sorting option")}, + want: &Commit{ + Type: "feat", + Scope: "user", + Subject: "add user sorting option", + }, + }, + { + name: "type and breaking", + args: args{header: []byte("feat!: add user sorting option")}, + want: &Commit{ + Type: "feat", + Subject: "add user sorting option", + IsBreaking: true, + }, + }, + { + name: "type, scope and breaking", + args: args{header: []byte("feat(user)!: add user sorting option")}, + want: &Commit{ + Type: "feat", + Scope: "user", + Subject: "add user sorting option", + IsBreaking: true, + }, + }, + { + name: "type with underscore (_)", + args: args{header: []byte("int_feat: add user sorting option")}, + want: &Commit{ + Type: "int_feat", + Subject: "add user sorting option", + }, + }, + { + name: "type with hyphen (-)", + args: args{header: []byte("int-feat: add user sorting option")}, + want: &Commit{ + Type: "int-feat", + Subject: "add user sorting option", + }, + }, + { + name: "scope with underscopre (_)", + args: args{ + header: []byte("feat(user_sort): add user sorting option"), + }, + want: &Commit{ + Type: "feat", + Scope: "user_sort", + Subject: "add user sorting option", + }, + }, + { + name: "scope with hyphen (-)", + args: args{ + header: []byte("feat(user-sort): add user sorting option"), + }, + want: &Commit{ + Type: "feat", + Scope: "user-sort", + Subject: "add user sorting option", + }, + }, + { + name: "scope with slash (/)", + args: args{ + header: []byte("feat(user/sort): add user sorting option"), + }, + want: &Commit{ + Type: "feat", + Scope: "user/sort", + Subject: "add user sorting option", + }, + }, + { + name: "scope with period (.)", + args: args{ + header: []byte("feat(user.sort): add user sorting option"), + }, + want: &Commit{ + Type: "feat", + Scope: "user.sort", + Subject: "add user sorting option", + }, + }, + { + name: "scope with dollar sign ($)", + args: args{ + header: []byte("feat($user): add user sorting option"), + }, + want: &Commit{ + Type: "feat", + Scope: "$user", + Subject: "add user sorting option", + }, + }, + { + name: "scope with star (*)", + args: args{ + header: []byte("feat(user*): add user sorting option"), + }, + want: &Commit{ + Type: "feat", + Scope: "user*", + Subject: "add user sorting option", + }, + }, + { + name: "scope with space ( )", + args: args{ + header: []byte("feat(user sort): add user sorting option"), + }, + want: &Commit{ + Type: "feat", + Scope: "user sort", + Subject: "add user sorting option", + }, + }, + { + name: "multi-line header (LF)", + args: args{ + header: []byte("feat(user)!: add usersorting\noption"), + }, + errStr: "header cannot span multiple lines", + }, + { + name: "multi-line header (CR)", + args: args{ + header: []byte("feat(user)!: add usersorting\roption"), + }, + errStr: "header cannot span multiple lines", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseHeader(tt.args.header) + + if tt.errStr != "" { + assert.Error(t, err, tt.errStr) + } else { + assert.Equal(t, tt.want, got) + } + }) + } +} + func Test_paragraph(t *testing.T) { type args struct { input []byte