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"
2022-02-06 23:16:52 +00:00
"strconv"
2019-11-17 16:17:24 +00:00
"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 (
2022-03-07 15:11:07 +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
2021-09-24 20:59:38 +00:00
Tag ( version semver . Version ) ( string , 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 {
2022-02-06 23:16:52 +00:00
Date string ` json:"date,omitempty" `
Timestamp int ` json:"timestamp,omitempty" `
AuthorName string ` json:"authorName,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"
2021-07-31 20:44:42 +00:00
DateRange LogRangeType = "date"
HashRange LogRangeType = "hash"
2021-01-25 05:51:42 +00:00
)
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.
2022-04-07 09:12:14 +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 ,
2022-04-07 09:12:14 +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.
2022-04-04 06:25:17 +00:00
func ( g GitImpl ) LastTag ( ) string {
cmd := exec . Command ( "git" , "for-each-ref" , "refs/tags/" + g . tagCfg . Filter , "--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 ) {
2022-02-06 23:16:52 +00:00
format := "--pretty=format:\"%ad" + logSeparator + "%at" + logSeparator + "%cN" + logSeparator + "%h" + logSeparator + "%s" + logSeparator + "%b" + endLine + "\""
2021-01-25 05:51:42 +00:00
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
}
2022-04-07 09:12:14 +00:00
logs , parseErr := parseLogOutput ( g . messageProcessor , string ( out ) )
2022-04-06 09:01:17 +00:00
if parseErr != nil {
return nil , parseErr
}
return logs , 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.
2021-09-24 20:59:38 +00:00
func ( g GitImpl ) Tag ( version semver . Version ) ( string , 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 )
2021-09-24 20:49:58 +00:00
if out , err := tagCommand . CombinedOutput ( ) ; err != nil {
2021-09-24 20:59:38 +00:00
return tag , combinedOutputErr ( err , out )
2019-12-04 22:37:50 +00:00
}
pushCommand := exec . Command ( "git" , "push" , "origin" , tag )
2021-09-24 20:49:58 +00:00
if out , err := pushCommand . CombinedOutput ( ) ; err != nil {
2021-09-24 20:59:38 +00:00
return tag , combinedOutputErr ( err , out )
2021-09-24 20:49:58 +00:00
}
2021-09-24 20:59:38 +00:00
return tag , nil
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 ) {
2022-03-07 15:11:07 +00:00
cmd := exec . Command ( "git" , "for-each-ref" , "--sort" , "creatordate" , "--format" , "%(creatordate:iso8601)#%(refname:short)" , "refs/tags/" + g . tagCfg . Filter )
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
}
2022-04-07 09:12:14 +00:00
func parseLogOutput ( messageProcessor MessageProcessor , log string ) ( [ ] GitCommitLog , error ) {
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 != "" {
2022-04-04 06:55:29 +00:00
log , err := parseCommitLog ( messageProcessor , text )
2022-04-07 09:12:14 +00:00
if err != nil {
return nil , err
2022-04-04 06:55:29 +00:00
}
2022-04-07 09:12:14 +00:00
logs = append ( logs , log )
2019-11-17 16:17:24 +00:00
}
}
2022-04-06 09:01:17 +00:00
return logs , nil
2019-11-17 16:17:24 +00:00
}
2022-04-04 06:55:29 +00:00
func parseCommitLog ( messageProcessor MessageProcessor , commit string ) ( GitCommitLog , error ) {
2019-11-17 16:17:24 +00:00
content := strings . Split ( strings . Trim ( commit , "\"" ) , logSeparator )
2022-02-06 23:16:52 +00:00
timestamp , _ := strconv . Atoi ( content [ 1 ] )
2022-04-04 06:55:29 +00:00
message , err := messageProcessor . Parse ( content [ 4 ] , content [ 5 ] )
if err != nil {
return GitCommitLog { } , err
}
2019-11-17 16:17:24 +00:00
return GitCommitLog {
2022-02-06 23:16:52 +00:00
Date : content [ 0 ] ,
Timestamp : timestamp ,
AuthorName : content [ 2 ] ,
Hash : content [ 3 ] ,
2022-04-04 06:55:29 +00:00
Message : message ,
} , nil
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 ] )
}