240 lines
5.4 KiB
Go
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
|
|
}
|