Skip to content

Commit

Permalink
refactor: use FSM for git output parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
HandOfGod94 committed Sep 9, 2023
1 parent 0fbd10b commit b30c543
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 78 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/handofgod94/gh-jira-changelog
go 1.20

require (
github.com/looplab/fsm v1.0.1
github.com/samber/lo v1.38.1
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.16.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/looplab/fsm v1.0.1 h1:OEW0ORrIx095N/6lgoGkFkotqH6s7vaFPsgjLAaF5QU=
github.com/looplab/fsm v1.0.1/go.mod h1:PmD3fFvQEIsjMEfvZdrCDZ6y8VwKTwWNjlpEr6IKPO4=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
Expand Down
10 changes: 5 additions & 5 deletions pkg/jira_changelog/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ func panicIfErr(err error) {
}

func (c Generator) Generate(ctx context.Context) *Changelog {
gitOutput, err := git.ExecGitLog(ctx, c.fromRef, c.toRef)
panicIfErr(err)

commits, err := gitOutput.Commits()
panicIfErr(err)
gcw := git.NewCommitParseWorkflow(c.fromRef, c.toRef)
commits, err := gcw.Commits(ctx)
if err != nil {
panic(fmt.Errorf("failed at \"%s\" state. %w. State: %+v", gcw.FSM.Current(), err, gcw))
}

changes, err := c.changesFromCommits(commits)
panicIfErr(err)
Expand Down
129 changes: 56 additions & 73 deletions pkg/jira_changelog/git/commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import (
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
"time"

"golang.org/x/exp/slog"
"github.com/looplab/fsm"
)

type Commit struct {
Expand All @@ -18,92 +16,77 @@ type Commit struct {
Sha string
}

type GitOutput string

var gitoutputPattern = regexp.MustCompile(`^\((\d+)\)\s+\{(\w+)\}\s*(.*)`)

func ExecGitLog(ctx context.Context, fromRef, toRef string) (GitOutput, error) {
cmd := exec.CommandContext(ctx, "git", "log", "--decorate-refs-exclude=refs/*", "--pretty=(%ct) {%h} %d %s", "--no-merges", fromRef+".."+toRef)
stdout, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to execute git command: %v", err)
}
const (
InitialState = "initial"
CommandExecuted = "command_executed"
OutuptParsed = "output_parsed"
)

result := string(stdout)
return GitOutput(result), nil
type commitParseWorkflow struct {
fromRef string
toRef string
gitOutput GitOutput
commits []Commit
FSM *fsm.FSM
}

func FetchGitRemoteURL(ctx context.Context) (string, error) {
cmd := exec.CommandContext(ctx, "git", "remote", "get-url", "origin")
stdout, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to execute git command: %v", err)
func NewCommitParseWorkflow(fromRef, toRef string) *commitParseWorkflow {
cpw := &commitParseWorkflow{
fromRef: fromRef,
toRef: toRef,
}

result := strings.TrimSpace(string(stdout))
result = strings.TrimSuffix(result, ".git")
return result, nil
cpw.FSM = fsm.NewFSM(
InitialState,
fsm.Events{
{Name: "execute_git_log", Src: []string{InitialState}, Dst: CommandExecuted},
{Name: "parse_output", Src: []string{CommandExecuted}, Dst: OutuptParsed},
},
fsm.Callbacks{
"before_execute_git_log": func(ctx context.Context, e *fsm.Event) {
ouptut, err := execGitLog(ctx, fromRef, toRef)
if err != nil {
e.Cancel(err)
return
}
cpw.gitOutput = ouptut
},
"before_parse_output": func(ctx context.Context, e *fsm.Event) {
commits, err := cpw.gitOutput.Commits()
if err != nil {
e.Cancel(err)
return
}
cpw.commits = commits
},
},
)
return cpw
}

func (gt GitOutput) Commits() ([]Commit, error) {
output := strings.TrimSpace(string(gt))
commits := make([]Commit, 0)
for _, line := range strings.Split(output, "\n") {
message, err := extractCommitMessage(line)
if err != nil {
slog.Error("failed to extract commit message", "gitlogLine", line)
return []Commit{}, fmt.Errorf("failed to extract commit message. %w", err)
}

commitTime, err := extractTime(line)
if err != nil {
slog.Error("failed to extract timestamp", "gitlogLine", line)
return []Commit{}, fmt.Errorf("failed to extract timestamp. %w", err)
}

sha, err := extractSha(line)
if err != nil {
slog.Error("failed to extract sha", "gitlogLine", line)
return []Commit{}, fmt.Errorf("failed to extract sha. %w", err)
}

commits = append(commits, Commit{
Message: message,
Time: commitTime,
Sha: sha,
})
func (cpw *commitParseWorkflow) Commits(ctx context.Context) ([]Commit, error) {
err := cpw.FSM.Event(ctx, "execute_git_log")
if err != nil {
return []Commit{}, fmt.Errorf("failed to execute git log. %w", err)
}
return commits, nil
}

func extractCommitMessage(gitlogLine string) (string, error) {
gitlogLine = strings.TrimSpace(gitlogLine)
result := gitoutputPattern.FindStringSubmatch(gitlogLine)
if len(result) < 4 {
return "", fmt.Errorf("couldn't find commit message in git log. %v", gitlogLine)
err = cpw.FSM.Event(ctx, "parse_output")
if err != nil {
return []Commit{}, fmt.Errorf("failed to parse output. %w", err)
}

return result[3], nil
return cpw.commits, nil
}

func extractTime(gitlogLine string) (time.Time, error) {
result := gitoutputPattern.FindStringSubmatch(gitlogLine)
if len(result) < 2 {
return time.Time{}, fmt.Errorf("couldn't find timestamp in commit message. %v", gitlogLine)
}

timestamp, err := strconv.Atoi(result[1])
func execGitLog(ctx context.Context, fromRef, toRef string) (GitOutput, error) {
cmd := exec.CommandContext(ctx, "git", "log", "--decorate-refs-exclude=refs/*", "--pretty=(%ct) {%h} %d %s", "--no-merges", fromRef+".."+toRef)
stdout, err := cmd.Output()
if err != nil {
return time.Time{}, fmt.Errorf("failed to extract timestamp in commit message. %w", err)
}
return time.Unix(int64(timestamp), 0), nil
}

func extractSha(gitlogLine string) (string, error) {
result := gitoutputPattern.FindStringSubmatch(gitlogLine)
if len(result) < 3 {
return "", fmt.Errorf("couldn't find sha in commit message. %v", gitlogLine)
return "", fmt.Errorf("failed to execute git command: %v", err)
}

return result[2], nil
result := string(stdout)
return GitOutput(result), nil
}
75 changes: 75 additions & 0 deletions pkg/jira_changelog/git/git_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package git

import (
"fmt"
"strconv"
"strings"
"time"

"golang.org/x/exp/slog"
)

type GitOutput string

func (gt GitOutput) Commits() ([]Commit, error) {
output := strings.TrimSpace(string(gt))
commits := make([]Commit, 0)
for _, line := range strings.Split(output, "\n") {
message, err := extractCommitMessage(line)
if err != nil {
slog.Error("failed to extract commit message", "gitlogLine", line)
return []Commit{}, fmt.Errorf("failed to extract commit message. %w", err)
}

commitTime, err := extractTime(line)
if err != nil {
slog.Error("failed to extract timestamp", "gitlogLine", line)
return []Commit{}, fmt.Errorf("failed to extract timestamp. %w", err)
}

sha, err := extractSha(line)
if err != nil {
slog.Error("failed to extract sha", "gitlogLine", line)
return []Commit{}, fmt.Errorf("failed to extract sha. %w", err)
}

commits = append(commits, Commit{
Message: message,
Time: commitTime,
Sha: sha,
})
}
return commits, nil
}

func extractCommitMessage(gitlogLine string) (string, error) {
gitlogLine = strings.TrimSpace(gitlogLine)
result := gitoutputPattern.FindStringSubmatch(gitlogLine)
if len(result) < 4 {
return "", fmt.Errorf("couldn't find commit message in git log. %v", gitlogLine)
}

return result[3], nil
}

func extractTime(gitlogLine string) (time.Time, error) {
result := gitoutputPattern.FindStringSubmatch(gitlogLine)
if len(result) < 2 {
return time.Time{}, fmt.Errorf("couldn't find timestamp in commit message. %v", gitlogLine)
}

timestamp, err := strconv.Atoi(result[1])
if err != nil {
return time.Time{}, fmt.Errorf("failed to extract timestamp in commit message. %w", err)
}
return time.Unix(int64(timestamp), 0), nil
}

func extractSha(gitlogLine string) (string, error) {
result := gitoutputPattern.FindStringSubmatch(gitlogLine)
if len(result) < 3 {
return "", fmt.Errorf("couldn't find sha in commit message. %v", gitlogLine)
}

return result[2], nil
}

0 comments on commit b30c543

Please sign in to comment.