240 lines
5.4 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package funcs
import (
"fmt"
"io"
"io/ioutil"
"os/exec"
"time"
"github.com/go-git/go-git/v5"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
func VCSGitFuncs(path string) map[string]function.Function {
state := &VCSGit{Path: path}
return map[string]function.Function{
"gitrefpretty": state.RefPrettyFunc(),
"gitrefhash": state.RefHashFunc(),
"gitreftag": state.RefTagFunc(),
"gitremoteurl": state.RemoteUrlFunc(),
}
}
type VCSGit struct {
// Path of the git repository. Parent directories will be searched for
// a ".git" folder automatically.
Path string
initErr error
repo *git.Repository
}
// RefPrettyFunc returns a string format of the current Git ref. This function
// takes some liberties to humanize the output: it will use a tag if the
// ref matches a tag, it will append "+CHANGES" to the commit if there are
// uncommitted changed files, etc.
//
// You may use direct functions such as `gitrefhash` if you want the direct
// hash. Or `gitreftag` to get the current tag.
//
// vagrant:gitrefpretty
func (s *VCSGit) RefPrettyFunc() function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{},
Type: function.StaticReturnType(cty.String),
Impl: s.refPrettyFunc,
})
}
func (s *VCSGit) refPrettyFunc(args []cty.Value, retType cty.Type) (cty.Value, error) {
if err := s.init(); err != nil {
return cty.UnknownVal(cty.String), err
}
ref, err := s.repo.Head()
if err != nil {
return cty.UnknownVal(cty.String), err
}
result := ref.Hash().String()
// Get the tags
iter, err := s.repo.Tags()
if err != nil {
return cty.UnknownVal(cty.String), err
}
defer iter.Close()
for {
tagRef, err := iter.Next()
if err == io.EOF {
break
}
if err != nil {
return cty.UnknownVal(cty.String), err
}
if tagRef.Hash() == ref.Hash() {
result = tagRef.Name().Short()
break
}
}
// To determine if there are changes we subprocess because go-git's Status
// function is really, really slow sadly. On the vagrant repo at the time
// of this commit, go-git took 12s on my machine vs. 50ms for `git`.
cmd := exec.Command("git", "diff", "--quiet")
cmd.Stdout = ioutil.Discard
cmd.Stderr = ioutil.Discard
if err := cmd.Run(); err != nil {
exitError, ok := err.(*exec.ExitError)
if !ok {
return cty.UnknownVal(cty.String), fmt.Errorf("error executing git: %s", err)
}
if exitError.ExitCode() != 0 {
result += fmt.Sprintf("_CHANGES_%d", time.Now().Unix())
}
}
return cty.StringVal(result), nil
}
// RefHashFunc returns the full hash of the HEAD ref.
//
// vagrant:gitrefhash
func (s *VCSGit) RefHashFunc() function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{},
Type: function.StaticReturnType(cty.String),
Impl: s.refHashFunc,
})
}
func (s *VCSGit) refHashFunc(args []cty.Value, retType cty.Type) (cty.Value, error) {
if err := s.init(); err != nil {
return cty.UnknownVal(cty.String), err
}
ref, err := s.repo.Head()
if err != nil {
return cty.UnknownVal(cty.String), err
}
return cty.StringVal(ref.Hash().String()), nil
}
// RefTagFunc returns the tag of the HEAD ref or empty if not tag is found.
//
// vagrant:gitreftag
func (s *VCSGit) RefTagFunc() function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{},
Type: function.StaticReturnType(cty.String),
Impl: s.refTagFunc,
})
}
func (s *VCSGit) refTagFunc(args []cty.Value, retType cty.Type) (cty.Value, error) {
if err := s.init(); err != nil {
return cty.UnknownVal(cty.String), err
}
ref, err := s.repo.Head()
if err != nil {
return cty.UnknownVal(cty.String), err
}
// Get the tags
iter, err := s.repo.Tags()
if err != nil {
return cty.UnknownVal(cty.String), err
}
defer iter.Close()
for {
tagRef, err := iter.Next()
if err == io.EOF {
break
}
if err != nil {
return cty.UnknownVal(cty.String), err
}
if tagRef.Hash() == ref.Hash() {
return cty.StringVal(tagRef.Name().Short()), nil
}
}
return cty.StringVal(""), nil
}
// RemoteUrlFunc returns the URL for the matching remote or unknown
// if it can't be found.
//
// vagrant:gitremoteurl
func (s *VCSGit) RemoteUrlFunc() function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "name",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: s.remoteUrlFunc,
})
}
func (s *VCSGit) remoteUrlFunc(args []cty.Value, retType cty.Type) (cty.Value, error) {
if err := s.init(); err != nil {
return cty.UnknownVal(cty.String), err
}
name := args[0].AsString()
remote, err := s.repo.Remote(name)
if err != nil {
if err == git.ErrRemoteNotFound {
err = nil
}
return cty.UnknownVal(cty.String), err
}
urls := remote.Config().URLs
if len(urls) == 0 {
return cty.UnknownVal(cty.String), nil
}
return cty.StringVal(urls[0]), nil
}
func (s *VCSGit) init() error {
// If we initialized already return
if s.initErr != nil {
return s.initErr
}
if s.repo != nil {
return nil
}
// Check if `git` is installed. We'll use this sometimes.
if _, err := exec.LookPath("git"); err != nil {
s.initErr = fmt.Errorf("git was not found on the system and is required")
return s.initErr
}
// Open the repo
repo, err := git.PlainOpenWithOptions(s.Path, &git.PlainOpenOptions{
DetectDotGit: true,
})
if err != nil {
s.initErr = err
return err
}
s.repo = repo
return nil
}