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 }