2019-11-17 16:17:24 +00:00
package sv
import (
"bufio"
"bytes"
2021-03-04 03:42:51 +00:00
"errors"
2019-11-17 16:17:24 +00:00
"fmt"
2020-12-02 02:15:51 +00:00
"os"
2019-11-17 16:17:24 +00:00
"os/exec"
"strings"
2020-02-01 21:19:38 +00:00
"time"
2019-11-17 16:17:24 +00:00
2021-02-13 18:40:09 +00:00
"github.com/Masterminds/semver/v3"
2019-11-17 16:17:24 +00:00
)
const (
2020-02-02 00:15:12 +00:00
logSeparator = "##"
endLine = "~~"
2019-11-17 16:17:24 +00:00
)
2021-07-31 19:03:58 +00:00
// Git commands.
2019-11-17 16:17:24 +00:00
type Git interface {
2021-04-11 01:59:30 +00:00
LastTag ( ) string
2021-01-25 05:51:42 +00:00
Log ( lr LogRange ) ( [ ] GitCommitLog , error )
2020-12-02 02:15:51 +00:00
Commit ( header , body , footer string ) error
2019-11-17 16:17:24 +00:00
Tag ( version semver . Version ) error
2020-02-01 21:19:38 +00:00
Tags ( ) ( [ ] GitTag , error )
2020-08-28 01:57:55 +00:00
Branch ( ) string
2021-03-04 03:42:51 +00:00
IsDetached ( ) ( bool , error )
2019-11-17 16:17:24 +00:00
}
2021-07-31 19:03:58 +00:00
// GitCommitLog description of a single commit log.
2019-11-17 16:17:24 +00:00
type GitCommitLog struct {
2021-02-14 02:35:31 +00:00
Date string ` json:"date,omitempty" `
Hash string ` json:"hash,omitempty" `
Message CommitMessage ` json:"message,omitempty" `
2019-11-17 16:17:24 +00:00
}
2021-07-31 19:03:58 +00:00
// GitTag git tag info.
2020-02-01 21:19:38 +00:00
type GitTag struct {
Name string
Date time . Time
}
2021-07-31 19:03:58 +00:00
// LogRangeType type of log range.
2021-01-25 05:51:42 +00:00
type LogRangeType string
2021-07-31 19:03:58 +00:00
// constants for log range type.
2021-01-25 05:51:42 +00:00
const (
TagRange LogRangeType = "tag"
DateRange = "date"
HashRange = "hash"
)
2021-07-31 19:03:58 +00:00
// LogRange git log range.
2021-01-25 05:51:42 +00:00
type LogRange struct {
rangeType LogRangeType
start string
end string
}
2021-07-31 19:03:58 +00:00
// NewLogRange LogRange constructor.
2021-01-25 05:51:42 +00:00
func NewLogRange ( t LogRangeType , start , end string ) LogRange {
return LogRange { rangeType : t , start : start , end : end }
}
2021-07-31 19:03:58 +00:00
// GitImpl git command implementation.
2019-11-17 16:17:24 +00:00
type GitImpl struct {
2021-02-14 04:04:32 +00:00
messageProcessor MessageProcessor
2021-02-14 05:32:23 +00:00
tagCfg TagConfig
2019-11-17 16:17:24 +00:00
}
2021-07-31 19:03:58 +00:00
// NewGit constructor.
2021-02-14 05:32:23 +00:00
func NewGit ( messageProcessor MessageProcessor , cfg TagConfig ) * GitImpl {
2020-02-01 22:43:02 +00:00
return & GitImpl {
2021-02-14 02:49:24 +00:00
messageProcessor : messageProcessor ,
2021-02-14 05:32:23 +00:00
tagCfg : cfg ,
2020-02-01 22:43:02 +00:00
}
2019-11-17 16:17:24 +00:00
}
2021-07-31 19:03:58 +00:00
// LastTag get last tag, if no tag found, return empty.
2021-04-11 01:59:30 +00:00
func ( GitImpl ) LastTag ( ) string {
cmd := exec . Command ( "git" , "for-each-ref" , "refs/tags" , "--sort" , "-creatordate" , "--format" , "%(refname:short)" , "--count" , "1" )
2019-11-17 16:17:24 +00:00
out , err := cmd . CombinedOutput ( )
if err != nil {
return ""
}
return strings . TrimSpace ( strings . Trim ( string ( out ) , "\n" ) )
}
2021-07-31 19:03:58 +00:00
// Log return git log.
2021-01-25 05:51:42 +00:00
func ( g GitImpl ) Log ( lr LogRange ) ( [ ] GitCommitLog , error ) {
format := "--pretty=format:\"%ad" + logSeparator + "%h" + logSeparator + "%s" + logSeparator + "%b" + endLine + "\""
params := [ ] string { "log" , "--date=short" , format }
if lr . start != "" || lr . end != "" {
switch lr . rangeType {
case DateRange :
params = append ( params , "--since" , lr . start , "--until" , addDay ( lr . end ) )
default :
if lr . start == "" {
params = append ( params , lr . end )
} else {
params = append ( params , lr . start + ".." + str ( lr . end , "HEAD" ) )
}
}
2019-11-17 16:17:24 +00:00
}
2021-01-25 05:51:42 +00:00
cmd := exec . Command ( "git" , params ... )
2019-11-17 16:17:24 +00:00
out , err := cmd . CombinedOutput ( )
if err != nil {
2021-01-25 05:51:42 +00:00
return nil , combinedOutputErr ( err , out )
2019-11-17 16:17:24 +00:00
}
2021-02-14 02:49:24 +00:00
return parseLogOutput ( g . messageProcessor , string ( out ) ) , nil
2019-11-17 16:17:24 +00:00
}
2021-07-31 19:03:58 +00:00
// Commit runs git commit.
2020-12-02 02:15:51 +00:00
func ( g GitImpl ) Commit ( header , body , footer string ) error {
cmd := exec . Command ( "git" , "commit" , "-m" , header , "-m" , "" , "-m" , body , "-m" , "" , "-m" , footer )
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
return cmd . Run ( )
}
2021-07-31 19:03:58 +00:00
// Tag create a git tag.
2019-11-17 16:17:24 +00:00
func ( g GitImpl ) Tag ( version semver . Version ) error {
2021-02-14 05:32:23 +00:00
tag := fmt . Sprintf ( g . tagCfg . Pattern , version . Major ( ) , version . Minor ( ) , version . Patch ( ) )
2019-12-04 22:50:02 +00:00
tagMsg := fmt . Sprintf ( "Version %d.%d.%d" , version . Major ( ) , version . Minor ( ) , version . Patch ( ) )
2019-12-04 22:37:50 +00:00
2019-12-04 22:50:02 +00:00
tagCommand := exec . Command ( "git" , "tag" , "-a" , tag , "-m" , tagMsg )
2019-12-04 22:37:50 +00:00
if err := tagCommand . Run ( ) ; err != nil {
return err
}
pushCommand := exec . Command ( "git" , "push" , "origin" , tag )
return pushCommand . Run ( )
2019-11-17 16:17:24 +00:00
}
2021-07-31 19:03:58 +00:00
// Tags list repository tags.
2020-02-01 21:19:38 +00:00
func ( g GitImpl ) Tags ( ) ( [ ] GitTag , error ) {
2021-04-10 20:01:58 +00:00
cmd := exec . Command ( "git" , "for-each-ref" , "--sort" , "creatordate" , "--format" , "%(creatordate:iso8601)#%(refname:short)" , "refs/tags" )
2020-02-01 21:19:38 +00:00
out , err := cmd . CombinedOutput ( )
if err != nil {
2021-01-25 05:51:42 +00:00
return nil , combinedOutputErr ( err , out )
2020-02-01 21:19:38 +00:00
}
return parseTagsOutput ( string ( out ) )
}
2021-07-31 19:03:58 +00:00
// Branch get git branch.
2020-08-28 01:57:55 +00:00
func ( GitImpl ) Branch ( ) string {
cmd := exec . Command ( "git" , "symbolic-ref" , "--short" , "HEAD" )
out , err := cmd . CombinedOutput ( )
if err != nil {
return ""
}
return strings . TrimSpace ( strings . Trim ( string ( out ) , "\n" ) )
}
2021-03-04 03:42:51 +00:00
// IsDetached check if is detached.
func ( GitImpl ) IsDetached ( ) ( bool , error ) {
cmd := exec . Command ( "git" , "symbolic-ref" , "-q" , "HEAD" )
out , err := cmd . CombinedOutput ( )
if output := string ( out ) ; err != nil { //-q: do not issue an error message if the <name> is not a symbolic ref, but a detached HEAD; instead exit with non-zero status silently.
if output == "" {
return true , nil
}
return false , errors . New ( output )
}
return false , nil
}
2020-02-01 21:19:38 +00:00
func parseTagsOutput ( input string ) ( [ ] GitTag , error ) {
scanner := bufio . NewScanner ( strings . NewReader ( input ) )
var result [ ] GitTag
for scanner . Scan ( ) {
if line := strings . TrimSpace ( scanner . Text ( ) ) ; line != "" {
values := strings . Split ( line , "#" )
2020-05-01 03:45:08 +00:00
date , _ := time . Parse ( "2006-01-02 15:04:05 -0700" , values [ 0 ] ) // ignore invalid dates
2020-02-01 21:19:38 +00:00
result = append ( result , GitTag { Name : values [ 1 ] , Date : date } )
}
}
return result , nil
}
2021-02-14 04:04:32 +00:00
func parseLogOutput ( messageProcessor MessageProcessor , log string ) [ ] GitCommitLog {
2019-11-17 16:17:24 +00:00
scanner := bufio . NewScanner ( strings . NewReader ( log ) )
scanner . Split ( splitAt ( [ ] byte ( endLine ) ) )
var logs [ ] GitCommitLog
for scanner . Scan ( ) {
if text := strings . TrimSpace ( strings . Trim ( scanner . Text ( ) , "\"" ) ) ; text != "" {
2021-02-14 02:49:24 +00:00
logs = append ( logs , parseCommitLog ( messageProcessor , text ) )
2019-11-17 16:17:24 +00:00
}
}
return logs
}
2021-02-14 04:04:32 +00:00
func parseCommitLog ( messageProcessor MessageProcessor , commit string ) GitCommitLog {
2019-11-17 16:17:24 +00:00
content := strings . Split ( strings . Trim ( commit , "\"" ) , logSeparator )
return GitCommitLog {
2021-02-14 02:35:31 +00:00
Date : content [ 0 ] ,
Hash : content [ 1 ] ,
2021-02-14 02:49:24 +00:00
Message : messageProcessor . Parse ( content [ 2 ] , content [ 3 ] ) ,
2019-11-17 16:17:24 +00:00
}
}
func splitAt ( b [ ] byte ) func ( data [ ] byte , atEOF bool ) ( advance int , token [ ] byte , err error ) {
return func ( data [ ] byte , atEOF bool ) ( advance int , token [ ] byte , err error ) {
dataLen := len ( data )
if atEOF && dataLen == 0 {
return 0 , nil , nil
}
if i := bytes . Index ( data , b ) ; i >= 0 {
return i + len ( b ) , data [ 0 : i ] , nil
}
if atEOF {
return dataLen , data , nil
}
return 0 , nil , nil
}
}
2021-01-25 05:51:42 +00:00
func addDay ( value string ) string {
if value == "" {
return value
}
t , err := time . Parse ( "2006-01-02" , value )
if err != nil { // keep original value if is not date format
return value
}
return t . AddDate ( 0 , 0 , 1 ) . Format ( "2006-01-02" )
}
func str ( value , defaultValue string ) string {
if value != "" {
return value
}
return defaultValue
}
func combinedOutputErr ( err error , out [ ] byte ) error {
msg := strings . Split ( string ( out ) , "\n" )
return fmt . Errorf ( "%v - %s" , err , msg [ 0 ] )
}