github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/util/gitutil/gitutil.go (about)

     1  package gitutil
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"net/url"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/docker/buildx/util/osutil"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  // Git represents an active git object
    17  type Git struct {
    18  	ctx     context.Context
    19  	wd      string
    20  	gitpath string
    21  }
    22  
    23  // Option provides a variadic option for configuring the git client.
    24  type Option func(b *Git)
    25  
    26  // WithContext sets context.
    27  func WithContext(ctx context.Context) Option {
    28  	return func(b *Git) {
    29  		b.ctx = ctx
    30  	}
    31  }
    32  
    33  // WithWorkingDir sets working directory.
    34  func WithWorkingDir(wd string) Option {
    35  	return func(b *Git) {
    36  		b.wd = wd
    37  	}
    38  }
    39  
    40  // New initializes a new git client
    41  func New(opts ...Option) (*Git, error) {
    42  	var err error
    43  	c := &Git{
    44  		ctx: context.Background(),
    45  	}
    46  
    47  	for _, opt := range opts {
    48  		opt(c)
    49  	}
    50  
    51  	c.gitpath, err = gitPath(c.wd)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	return c, nil
    57  }
    58  
    59  func (c *Git) IsInsideWorkTree() bool {
    60  	out, err := c.clean(c.run("rev-parse", "--is-inside-work-tree"))
    61  	return out == "true" && err == nil
    62  }
    63  
    64  func (c *Git) IsDirty() bool {
    65  	out, err := c.run("status", "--porcelain", "--ignored")
    66  	return strings.TrimSpace(out) != "" || err != nil
    67  }
    68  
    69  func (c *Git) RootDir() (string, error) {
    70  	root, err := c.clean(c.run("rev-parse", "--show-toplevel"))
    71  	if err != nil {
    72  		return "", err
    73  	}
    74  	return osutil.SanitizePath(root), nil
    75  }
    76  
    77  func (c *Git) GitDir() (string, error) {
    78  	dir, err := c.RootDir()
    79  	if err != nil {
    80  		return "", err
    81  	}
    82  	return filepath.Join(dir, ".git"), nil
    83  }
    84  
    85  func (c *Git) RemoteURL() (string, error) {
    86  	// Try default remote based on remote tracking branch
    87  	if remote, err := c.currentRemote(); err == nil && remote != "" {
    88  		if ru, err := c.clean(c.run("remote", "get-url", remote)); err == nil && ru != "" {
    89  			return stripCredentials(ru), nil
    90  		}
    91  	}
    92  	// Next try to get the remote URL from the origin remote first
    93  	if ru, err := c.clean(c.run("remote", "get-url", "origin")); err == nil && ru != "" {
    94  		return stripCredentials(ru), nil
    95  	}
    96  	// If that fails, try to get the remote URL from the upstream remote
    97  	if ru, err := c.clean(c.run("remote", "get-url", "upstream")); err == nil && ru != "" {
    98  		return stripCredentials(ru), nil
    99  	}
   100  	return "", errors.New("no remote URL found for either origin or upstream")
   101  }
   102  
   103  func (c *Git) FullCommit() (string, error) {
   104  	return c.clean(c.run("show", "--format=%H", "HEAD", "--quiet", "--"))
   105  }
   106  
   107  func (c *Git) ShortCommit() (string, error) {
   108  	return c.clean(c.run("show", "--format=%h", "HEAD", "--quiet", "--"))
   109  }
   110  
   111  func (c *Git) Tag() (string, error) {
   112  	var tag string
   113  	var err error
   114  	for _, fn := range []func() (string, error){
   115  		func() (string, error) {
   116  			return c.clean(c.run("tag", "--points-at", "HEAD", "--sort", "-version:creatordate"))
   117  		},
   118  		func() (string, error) {
   119  			return c.clean(c.run("describe", "--tags", "--abbrev=0"))
   120  		},
   121  	} {
   122  		tag, err = fn()
   123  		if tag != "" || err != nil {
   124  			return tag, err
   125  		}
   126  	}
   127  	return tag, err
   128  }
   129  
   130  func (c *Git) run(args ...string) (string, error) {
   131  	var extraArgs = []string{
   132  		"-c", "log.showSignature=false",
   133  	}
   134  
   135  	args = append(extraArgs, args...)
   136  	cmd := exec.CommandContext(c.ctx, c.gitpath, args...)
   137  	if c.wd != "" {
   138  		cmd.Dir = c.wd
   139  	}
   140  
   141  	// Override the locale to ensure consistent output
   142  	cmd.Env = append(os.Environ(), "LC_ALL=C")
   143  
   144  	stdout := bytes.Buffer{}
   145  	stderr := bytes.Buffer{}
   146  	cmd.Stdout = &stdout
   147  	cmd.Stderr = &stderr
   148  
   149  	if err := cmd.Run(); err != nil {
   150  		return "", errors.New(stderr.String())
   151  	}
   152  	return stdout.String(), nil
   153  }
   154  
   155  func (c *Git) clean(out string, err error) (string, error) {
   156  	out = strings.ReplaceAll(strings.Split(out, "\n")[0], "'", "")
   157  	if err != nil {
   158  		err = errors.New(strings.TrimSuffix(err.Error(), "\n"))
   159  	}
   160  	return out, err
   161  }
   162  
   163  func (c *Git) currentRemote() (string, error) {
   164  	symref, err := c.clean(c.run("symbolic-ref", "-q", "HEAD"))
   165  	if err != nil {
   166  		return "", err
   167  	}
   168  	if symref == "" {
   169  		return "", nil
   170  	}
   171  	// git for-each-ref --format='%(upstream:remotename)'
   172  	remote, err := c.clean(c.run("for-each-ref", "--format=%(upstream:remotename)", symref))
   173  	if err != nil {
   174  		return "", err
   175  	}
   176  	return remote, nil
   177  }
   178  
   179  func IsUnknownRevision(err error) bool {
   180  	if err == nil {
   181  		return false
   182  	}
   183  	// https://github.com/git/git/blob/a6a323b31e2bcbac2518bddec71ea7ad558870eb/setup.c#L204
   184  	errMsg := strings.ToLower(err.Error())
   185  	return strings.Contains(errMsg, "unknown revision or path not in the working tree") || strings.Contains(errMsg, "bad revision")
   186  }
   187  
   188  // stripCredentials takes a URL and strips username and password from it.
   189  // e.g. "https://user:password@host.tld/path.git" will be changed to
   190  // "https://host.tld/path.git".
   191  // TODO: remove this function once fix from BuildKit is vendored here
   192  func stripCredentials(s string) string {
   193  	ru, err := url.Parse(s)
   194  	if err != nil {
   195  		return s // string is not a URL, just return it
   196  	}
   197  	ru.User = nil
   198  	return ru.String()
   199  }