github.com/arsham/gitrelease@v0.3.2-0.20221207124258-6867180b2c2d/commit/commit.go (about)

     1  package commit
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  )
     8  
     9  var (
    10  	descRe = regexp.MustCompile(`^\s*([[:alpha:]]+!?)\(?([[:alpha:],_-]+)?\)?(!)?:?(.*)`)
    11  	refRe  = regexp.MustCompile(`[[:alpha:]]+\s+#\d+`)
    12  )
    13  
    14  // ItemPrefix is the markdown prefix before each item.
    15  var ItemPrefix = "- "
    16  
    17  // A Group is a commit with all of its messages.
    18  type Group struct {
    19  	raw         string
    20  	Verb        string
    21  	Subject     string
    22  	Description string
    23  	Breaking    bool
    24  }
    25  
    26  // GroupFromCommit creates a Group object from the given line.
    27  func GroupFromCommit(msg string) Group {
    28  	matches := descRe.FindStringSubmatch(msg)
    29  	verb := matches[1]
    30  	subject := matches[2]
    31  	verbBreak := matches[3]
    32  	desc := matches[4]
    33  
    34  	breaking := false
    35  	if strings.HasSuffix(verb, "!") || verbBreak != "" {
    36  		breaking = true
    37  		verb = strings.TrimSuffix(verb, "!")
    38  	}
    39  
    40  	switch strings.ToLower(verb) {
    41  	case "ref", "refactor":
    42  		verb = "Refactor"
    43  	case "feat", "feature":
    44  		verb = "Feature"
    45  	case "fix", "fixed":
    46  		verb = "Fix"
    47  	case "chore":
    48  		verb = "Chore"
    49  	case "enhance", "enhancements", "enhancement":
    50  		verb = "Enhancements"
    51  	case "upgrade":
    52  		verb = "Upgrades"
    53  	case "ci":
    54  		verb = "CI"
    55  	case "style":
    56  		verb = "Style"
    57  	case "docs":
    58  		verb = "Docs"
    59  	default:
    60  		verb = ""
    61  	}
    62  
    63  	if verb == "" {
    64  		verb = "Misc"
    65  	}
    66  	if desc == "" {
    67  		desc = matches[0]
    68  	}
    69  
    70  	return Group{
    71  		raw:         msg,
    72  		Verb:        verb,
    73  		Subject:     subject,
    74  		Description: strings.TrimSpace(desc),
    75  		Breaking:    breaking,
    76  	}
    77  }
    78  
    79  // Section returns a printable line for the section.
    80  func (g Group) Section() string {
    81  	return "### " + upperFirst(g.Verb)
    82  }
    83  
    84  // DescriptionString returns a string that is suitable for printing a line in a
    85  // Group.
    86  func (g Group) DescriptionString() string {
    87  	subject := g.Subject
    88  	if strings.EqualFold(subject, "ci") {
    89  		subject = "CI"
    90  	}
    91  	if subject != "" {
    92  		subjects := strings.Split(subject, ",")
    93  		for i := range subjects {
    94  			subjects[i] = upperFirst(subjects[i])
    95  		}
    96  		subject = strings.Join(subjects, ",")
    97  		subject = "**" + subject + ":** "
    98  	}
    99  
   100  	lines := strings.Split(g.Description, `\n`)
   101  	refs := make([]string, 0, len(lines))
   102  	title := lines[0]
   103  	for _, line := range lines[1:] {
   104  		if line == "" {
   105  			continue
   106  		}
   107  		matches := refRe.FindAllStringSubmatch(line, -1)
   108  		for _, match := range matches {
   109  			refs = append(refs, match...)
   110  		}
   111  	}
   112  
   113  	title = strings.TrimPrefix(title, " ")
   114  	var ref string
   115  	if len(refs) > 0 {
   116  		ref = fmt.Sprintf(" (%s)", strings.Join(refs, ", "))
   117  	}
   118  	return fmt.Sprintf("- %s%s%s", subject, upperFirst(title), ref)
   119  }
   120  
   121  // ParseGroups parses the lines in the logs and returns them as a string.
   122  func ParseGroups(logs []string) string {
   123  	logs = cleanup(logs)
   124  	groups := make(map[string][]Group, len(logs))
   125  	for _, line := range logs {
   126  		group := GroupFromCommit(line)
   127  		groups[group.Verb] = append(groups[group.Verb], group)
   128  	}
   129  
   130  	buf := &strings.Builder{}
   131  	i := 0
   132  	for _, desc := range groups {
   133  		fmt.Fprintln(buf, desc[0].Section()+"\n")
   134  		for _, line := range desc {
   135  			fmt.Fprint(buf, line.DescriptionString())
   136  			if line.Breaking {
   137  				fmt.Fprintf(buf, " [**BREAKING CHANGE**]")
   138  			}
   139  			fmt.Fprintln(buf, "")
   140  		}
   141  		i++
   142  		if i < len(groups) {
   143  			fmt.Fprintf(buf, "\n\n")
   144  		}
   145  	}
   146  
   147  	str := buf.String()
   148  	return strings.TrimSuffix(str, "\n")
   149  }
   150  
   151  // cleanup returns only the title of the logs.
   152  func cleanup(logs []string) []string {
   153  	ret := make([]string, 0, len(logs))
   154  	for _, commit := range logs {
   155  		items := strings.Split(commit, "\n")
   156  		item := items[0]
   157  		breaking := false
   158  		for _, line := range items[1:] {
   159  			if strings.Contains(line, "BREAKING CHANGE") {
   160  				breaking = true
   161  			}
   162  			if !strings.Contains(line, "#") {
   163  				continue
   164  			}
   165  			item = fmt.Sprintf("%s (%s)", item, line)
   166  		}
   167  		if breaking {
   168  			item += " [**BREAKING CHANGE**]"
   169  		}
   170  		if item == "" {
   171  			continue
   172  		}
   173  		item = strings.TrimPrefix(item, " ")
   174  		ret = append(ret, item)
   175  	}
   176  	return ret
   177  }
   178  
   179  // upperFirst makes the first letter of the string an uppercase letter.
   180  func upperFirst(s string) string {
   181  	if s == "" {
   182  		return ""
   183  	}
   184  	return strings.ToUpper(s[:1]) + s[1:]
   185  }