0
0
mirror of https://github.com/thegeeklab/git-sv.git synced 2024-11-10 02:10:38 +00:00
git-sv/sv/git.go

260 lines
6.7 KiB
Go
Raw Normal View History

package sv
import (
"bufio"
"bytes"
"errors"
"fmt"
2020-12-02 02:15:51 +00:00
"os"
"os/exec"
"strconv"
"strings"
"time"
2021-02-13 18:40:09 +00:00
"github.com/Masterminds/semver/v3"
)
const (
logSeparator = "##"
endLine = "~~"
)
2021-07-31 19:03:58 +00:00
// Git commands.
type Git interface {
LastTag() string
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)
Tags() ([]GitTag, error)
Branch() string
IsDetached() (bool, error)
}
2021-07-31 19:03:58 +00:00
// GitCommitLog description of a single commit log.
type GitCommitLog struct {
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"`
}
2021-07-31 19:03:58 +00:00
// GitTag git tag info.
type GitTag struct {
Name string
Date time.Time
}
2021-07-31 19:03:58 +00:00
// LogRangeType type of log range.
type LogRangeType string
2021-07-31 19:03:58 +00:00
// constants for log range type.
const (
TagRange LogRangeType = "tag"
2021-07-31 20:44:42 +00:00
DateRange LogRangeType = "date"
HashRange LogRangeType = "hash"
)
2021-07-31 19:03:58 +00:00
// LogRange git log range.
type LogRange struct {
rangeType LogRangeType
start string
end string
}
2021-07-31 19:03:58 +00:00
// NewLogRange LogRange constructor.
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.
type GitImpl struct {
messageProcessor MessageProcessor
tagCfg TagConfig
}
2021-07-31 19:03:58 +00:00
// NewGit constructor.
func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl {
return &GitImpl{
2021-02-14 02:49:24 +00:00
messageProcessor: messageProcessor,
tagCfg: cfg,
}
}
2021-07-31 19:03:58 +00:00
// LastTag get last tag, if no tag found, return empty.
func (GitImpl) LastTag() string {
cmd := exec.Command("git", "for-each-ref", "refs/tags", "--sort", "-creatordate", "--format", "%(refname:short)", "--count", "1")
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.
func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
format := "--pretty=format:\"%ad" + logSeparator + "%at" + logSeparator + "%cN" + 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"))
}
}
}
cmd := exec.Command("git", params...)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, combinedOutputErr(err, out)
}
2021-02-14 02:49:24 +00:00
return parseLogOutput(g.messageProcessor, string(out)), nil
}
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) {
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
}
2021-07-31 19:03:58 +00:00
// Tags list repository tags.
func (g GitImpl) Tags() ([]GitTag, error) {
cmd := exec.Command("git", "for-each-ref", "--sort", "creatordate", "--format", "%(creatordate:iso8601)#%(refname:short)", "refs/tags")
out, err := cmd.CombinedOutput()
if err != nil {
return nil, combinedOutputErr(err, out)
}
return parseTagsOutput(string(out))
}
2021-07-31 19:03:58 +00:00
// Branch get git branch.
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"))
}
// 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
}
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
result = append(result, GitTag{Name: values[1], Date: date})
}
}
return result, nil
}
func parseLogOutput(messageProcessor MessageProcessor, log string) []GitCommitLog {
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))
}
}
return logs
}
func parseCommitLog(messageProcessor MessageProcessor, commit string) GitCommitLog {
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
timestamp, _ := strconv.Atoi(content[1])
return GitCommitLog{
Date: content[0],
Timestamp: timestamp,
AuthorName: content[2],
Hash: content[3],
Message: messageProcessor.Parse(content[4], content[5]),
}
}
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
}
}
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])
}