github.com/marianogappa/goreleaser@v0.26.2-0.20170715090149-96acd0a9fc46/pipeline/git/git.go (about)

     1  // Package git implements the Pipe interface getting and validating the
     2  // current git repository state
     3  package git
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"regexp"
     9  	"strings"
    10  	"text/template"
    11  	"time"
    12  
    13  	"github.com/apex/log"
    14  	"github.com/goreleaser/goreleaser/context"
    15  )
    16  
    17  // ErrInvalidVersionFormat is return when the version isnt in a valid format
    18  type ErrInvalidVersionFormat struct {
    19  	version string
    20  }
    21  
    22  func (e ErrInvalidVersionFormat) Error() string {
    23  	return fmt.Sprintf("%v is not in a valid version format", e.version)
    24  }
    25  
    26  // ErrDirty happens when the repo has uncommitted/unstashed changes
    27  type ErrDirty struct {
    28  	status string
    29  }
    30  
    31  func (e ErrDirty) Error() string {
    32  	return fmt.Sprintf("git is currently in a dirty state:\n%v", e.status)
    33  }
    34  
    35  // ErrWrongRef happens when the HEAD reference is different from the tag being built
    36  type ErrWrongRef struct {
    37  	commit, tag string
    38  }
    39  
    40  func (e ErrWrongRef) Error() string {
    41  	return fmt.Sprintf("git tag %v was not made against commit %v", e.tag, e.commit)
    42  }
    43  
    44  // ErrNoTag happens if the underlying git repository doesn't contain any tags
    45  // but no snapshot-release was requested.
    46  var ErrNoTag = fmt.Errorf("git doesn't contain any tags. Either add a tag or use --snapshot")
    47  
    48  // Pipe for brew deployment
    49  type Pipe struct{}
    50  
    51  // Description of the pipe
    52  func (Pipe) Description() string {
    53  	return "Getting and validating git state"
    54  }
    55  
    56  // Run the pipe
    57  func (Pipe) Run(ctx *context.Context) (err error) {
    58  	tag, commit, err := getInfo()
    59  	if err != nil {
    60  		return
    61  	}
    62  	if tag == "" && !ctx.Snapshot {
    63  		return ErrNoTag
    64  	}
    65  	ctx.Git = context.GitInfo{
    66  		CurrentTag: tag,
    67  		Commit:     commit,
    68  	}
    69  	if err = setLog(ctx, tag, commit); err != nil {
    70  		return
    71  	}
    72  	if err = setVersion(ctx, tag, commit); err != nil {
    73  		return
    74  	}
    75  	if !ctx.Validate {
    76  		log.Warn("skipped validations because --skip-validate is set")
    77  		return nil
    78  	}
    79  	return validate(ctx, commit, tag)
    80  }
    81  
    82  func setVersion(ctx *context.Context, tag, commit string) (err error) {
    83  	if ctx.Snapshot {
    84  		snapshotName, err := getSnapshotName(ctx, tag, commit)
    85  		if err != nil {
    86  			return fmt.Errorf("failed to generate snapshot name: %s", err.Error())
    87  		}
    88  		ctx.Version = snapshotName
    89  		return nil
    90  	}
    91  	// removes usual `v` prefix
    92  	ctx.Version = strings.TrimPrefix(tag, "v")
    93  	return
    94  }
    95  
    96  func setLog(ctx *context.Context, tag, commit string) (err error) {
    97  	if ctx.ReleaseNotes != "" {
    98  		return
    99  	}
   100  	var log string
   101  	if tag == "" {
   102  		log, err = getChangelog(commit)
   103  	} else {
   104  		log, err = getChangelog(tag)
   105  	}
   106  	if err != nil {
   107  		return err
   108  	}
   109  	ctx.ReleaseNotes = fmt.Sprintf("## Changelog\n\n%v", log)
   110  	return nil
   111  }
   112  
   113  type snapshotNameData struct {
   114  	Commit    string
   115  	Tag       string
   116  	Timestamp int64
   117  }
   118  
   119  func getSnapshotName(ctx *context.Context, tag, commit string) (string, error) {
   120  	tmpl, err := template.New("snapshot").Parse(ctx.Config.Snapshot.NameTemplate)
   121  	var out bytes.Buffer
   122  	if err != nil {
   123  		return "", err
   124  	}
   125  	var data = snapshotNameData{
   126  		Commit:    commit,
   127  		Tag:       tag,
   128  		Timestamp: time.Now().Unix(),
   129  	}
   130  	err = tmpl.Execute(&out, data)
   131  	return out.String(), err
   132  }
   133  
   134  func validate(ctx *context.Context, commit, tag string) error {
   135  	out, err := git("status", "--porcelain")
   136  	if strings.TrimSpace(out) != "" || err != nil {
   137  		return ErrDirty{out}
   138  	}
   139  	if ctx.Snapshot {
   140  		return nil
   141  	}
   142  	if !regexp.MustCompile("^[0-9.]+").MatchString(ctx.Version) {
   143  		return ErrInvalidVersionFormat{ctx.Version}
   144  	}
   145  	_, err = cleanGit("describe", "--exact-match", "--tags", "--match", tag)
   146  	if err != nil {
   147  		return ErrWrongRef{commit, tag}
   148  	}
   149  	return nil
   150  }
   151  
   152  func getChangelog(tag string) (string, error) {
   153  	prev, err := previous(tag)
   154  	if err != nil {
   155  		return "", err
   156  	}
   157  	if !prev.Tag {
   158  		return gitLog(prev.SHA, tag)
   159  	}
   160  	return gitLog(fmt.Sprintf("%v..%v", prev.SHA, tag))
   161  }
   162  
   163  func gitLog(refs ...string) (string, error) {
   164  	var args = []string{"log", "--pretty=oneline", "--abbrev-commit"}
   165  	args = append(args, refs...)
   166  	return git(args...)
   167  }
   168  
   169  func getInfo() (tag, commit string, err error) {
   170  	tag, err = cleanGit("describe", "--tags", "--abbrev=0")
   171  	if err != nil {
   172  		log.WithError(err).Info("failed to retrieve current tag")
   173  	}
   174  	commit, err = cleanGit("show", "--format='%H'", "HEAD")
   175  	return
   176  }
   177  
   178  func previous(tag string) (result ref, err error) {
   179  	result.Tag = true
   180  	result.SHA, err = cleanGit("describe", "--tags", "--abbrev=0", tag+"^")
   181  	if err != nil {
   182  		result.Tag = false
   183  		result.SHA, err = cleanGit("rev-list", "--max-parents=0", "HEAD")
   184  	}
   185  	return
   186  }
   187  
   188  type ref struct {
   189  	Tag bool
   190  	SHA string
   191  }