mirror of
https://github.com/moby/buildkit.git
synced 2026-08-09 09:11:45 +00:00
Preserve the git context subdirectory in SLSA configSource while keeping resolved materials scoped to the fetched repository ref. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
264 lines
6.7 KiB
Go
264 lines
6.7 KiB
Go
// Package dfgitutil provides Dockerfile-specific utilities for git refs.
|
|
package dfgitutil
|
|
|
|
import (
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
"github.com/moby/buildkit/util/gitutil"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// GitRef represents a git ref.
|
|
//
|
|
// Examples:
|
|
// - "https://github.com/foo/bar.git#baz/qux:quux/quuz" is parsed into:
|
|
// {Remote: "https://github.com/foo/bar.git", ShortName: "bar", Commit:"baz/qux", SubDir: "quux/quuz"}.
|
|
type GitRef struct {
|
|
// Remote is the remote repository path.
|
|
Remote string
|
|
|
|
// ShortName is the directory name of the repo.
|
|
// e.g., "bar" for "https://github.com/foo/bar.git"
|
|
ShortName string
|
|
|
|
// Ref is a commit hash, a tag, or branch name.
|
|
// Ref is optional.
|
|
Ref string
|
|
|
|
// Checksum is a commit hash.
|
|
Checksum string
|
|
|
|
// SubDir is a directory path inside the repo.
|
|
// SubDir is optional.
|
|
SubDir string
|
|
|
|
// IndistinguishableFromLocal is true for a ref that is indistinguishable from a local file path,
|
|
// e.g., "github.com/foo/bar".
|
|
//
|
|
// Deprecated.
|
|
// Instead, use a distinguishable form such as "https://github.com/foo/bar.git".
|
|
//
|
|
// The dockerfile frontend still accepts this form only for build contexts.
|
|
IndistinguishableFromLocal bool
|
|
|
|
// UnencryptedTCP is true for a ref that needs an unencrypted TCP connection,
|
|
// e.g., "git://..." and "http://..." .
|
|
//
|
|
// Discouraged, although not deprecated.
|
|
// Instead, consider using an encrypted TCP connection such as "git@github.com/foo/bar.git" or "https://github.com/foo/bar.git".
|
|
UnencryptedTCP bool
|
|
|
|
// KeepGitDir is true for URL that controls whether to keep the .git directory.
|
|
KeepGitDir *bool
|
|
|
|
// Submodules is true for URL that controls whether to fetch git submodules.
|
|
Submodules *bool
|
|
|
|
// MTime controls file modification time policy: "checkout" (default) or "commit".
|
|
MTime string
|
|
|
|
// FetchByCommit, when true, trusts Checksum as the commit and skips comparing
|
|
// it against the remote ref. The commit is fetched directly and the ref name
|
|
// (if any) is applied locally.
|
|
FetchByCommit bool
|
|
}
|
|
|
|
// ParseGitRef parses a git ref.
|
|
func ParseGitRef(ref string) (*GitRef, bool, error) {
|
|
res := &GitRef{}
|
|
|
|
var (
|
|
remote *gitutil.GitURL
|
|
err error
|
|
)
|
|
|
|
if strings.HasPrefix(ref, "./") || strings.HasPrefix(ref, "../") {
|
|
return nil, false, errors.WithStack(cerrdefs.ErrInvalidArgument)
|
|
} else if strings.HasPrefix(ref, "github.com/") {
|
|
res.IndistinguishableFromLocal = true // Deprecated
|
|
u, err := url.Parse(ref)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
u.Scheme = "https"
|
|
remote, err = gitutil.FromURL(u)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
} else {
|
|
remote, err = gitutil.ParseURL(ref)
|
|
if errors.Is(err, gitutil.ErrUnknownProtocol) {
|
|
return nil, false, err
|
|
}
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
switch remote.Scheme {
|
|
case gitutil.HTTPProtocol, gitutil.GitProtocol:
|
|
res.UnencryptedTCP = true // Discouraged, but not deprecated
|
|
}
|
|
|
|
switch remote.Scheme {
|
|
// An HTTP(S) URL is considered to be a valid git ref only when it has the ".git[...]" suffix.
|
|
case gitutil.HTTPProtocol, gitutil.HTTPSProtocol:
|
|
if !strings.HasSuffix(remote.Path, ".git") {
|
|
return nil, false, errors.WithStack(cerrdefs.ErrInvalidArgument)
|
|
}
|
|
}
|
|
}
|
|
|
|
res.Remote = remote.Remote
|
|
if res.IndistinguishableFromLocal {
|
|
_, res.Remote, _ = strings.Cut(res.Remote, "://")
|
|
}
|
|
if remote.Opts != nil {
|
|
res.Ref, res.SubDir = remote.Opts.Ref, remote.Opts.Subdir
|
|
}
|
|
|
|
repoSplitBySlash := strings.Split(res.Remote, "/")
|
|
res.ShortName = strings.TrimSuffix(repoSplitBySlash[len(repoSplitBySlash)-1], ".git")
|
|
|
|
if err := res.loadQuery(remote.Query); err != nil {
|
|
return nil, true, err
|
|
}
|
|
|
|
return res, true, nil
|
|
}
|
|
|
|
func (gf *GitRef) loadQuery(query url.Values) error {
|
|
if len(query) == 0 {
|
|
return nil
|
|
}
|
|
var tag, branch string
|
|
for k, v := range query {
|
|
switch len(v) {
|
|
case 0, 1:
|
|
if len(v) == 0 || v[0] == "" {
|
|
switch k {
|
|
case "submodules", "keep-git-dir", "fetch-by-commit":
|
|
v = nil
|
|
default:
|
|
return errors.Errorf("query %q has no value", k)
|
|
}
|
|
}
|
|
// NOP
|
|
default:
|
|
return errors.Errorf("query %q has multiple values", k)
|
|
}
|
|
switch k {
|
|
case "ref":
|
|
if gf.Ref != "" && gf.Ref != v[0] {
|
|
return errors.Errorf("ref conflicts: %q vs %q", gf.Ref, v[0])
|
|
}
|
|
gf.Ref = v[0]
|
|
case "tag":
|
|
tag = v[0]
|
|
case "branch":
|
|
branch = v[0]
|
|
case "subdir":
|
|
if gf.SubDir != "" && gf.SubDir != v[0] {
|
|
return errors.Errorf("subdir conflicts: %q vs %q", gf.SubDir, v[0])
|
|
}
|
|
gf.SubDir = v[0]
|
|
case "checksum", "commit":
|
|
gf.Checksum = v[0]
|
|
case "keep-git-dir":
|
|
var vv bool
|
|
if len(v) == 0 {
|
|
vv = true
|
|
} else {
|
|
var err error
|
|
vv, err = strconv.ParseBool(v[0])
|
|
if err != nil {
|
|
return errors.Errorf("invalid keep-git-dir value: %q", v[0])
|
|
}
|
|
}
|
|
gf.KeepGitDir = &vv
|
|
case "submodules":
|
|
var vv bool
|
|
if len(v) == 0 {
|
|
vv = true
|
|
} else {
|
|
var err error
|
|
vv, err = strconv.ParseBool(v[0])
|
|
if err != nil {
|
|
return errors.Errorf("invalid submodules value: %q", v[0])
|
|
}
|
|
}
|
|
gf.Submodules = &vv
|
|
case "mtime":
|
|
switch v[0] {
|
|
case "checkout", "commit":
|
|
gf.MTime = v[0]
|
|
default:
|
|
return errors.Errorf("invalid mtime value: %q (must be \"checkout\" or \"commit\")", v[0])
|
|
}
|
|
case "fetch-by-commit":
|
|
var vv bool
|
|
if len(v) == 0 {
|
|
vv = true
|
|
} else {
|
|
var err error
|
|
vv, err = strconv.ParseBool(v[0])
|
|
if err != nil {
|
|
return errors.Errorf("invalid fetch-by-commit value: %q", v[0])
|
|
}
|
|
}
|
|
gf.FetchByCommit = vv
|
|
default:
|
|
return errors.Errorf("unexpected query %q", k)
|
|
}
|
|
}
|
|
if tag != "" {
|
|
const tagPrefix = "refs/tags/"
|
|
if !strings.HasPrefix(tag, tagPrefix) {
|
|
tag = tagPrefix + tag
|
|
}
|
|
if gf.Ref != "" && gf.Ref != tag {
|
|
return errors.Errorf("ref conflicts: %q vs %q", gf.Ref, tag)
|
|
}
|
|
gf.Ref = tag
|
|
}
|
|
if branch != "" {
|
|
if tag != "" {
|
|
// TODO: consider allowing this, when the tag actually exists on the branch
|
|
return errors.New("branch conflicts with tag")
|
|
}
|
|
const branchPrefix = "refs/heads/"
|
|
if !strings.HasPrefix(branch, branchPrefix) {
|
|
branch = branchPrefix + branch
|
|
}
|
|
if gf.Ref != "" && gf.Ref != branch {
|
|
return errors.Errorf("ref conflicts: %q vs %q", gf.Ref, branch)
|
|
}
|
|
gf.Ref = branch
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FragmentFormat returns a simplified git URL in fragment format.
|
|
// If the URL cannot be parsed, the original string is returned with false.
|
|
func FragmentFormat(remote string, withSubdir bool) (string, bool) {
|
|
gitRef, _, err := ParseGitRef(remote)
|
|
if err != nil || gitRef == nil {
|
|
return remote, false
|
|
}
|
|
u := gitRef.Remote
|
|
subdir := ""
|
|
if withSubdir {
|
|
subdir = strings.TrimPrefix(path.Join("/", gitRef.SubDir), "/")
|
|
}
|
|
if gitRef.Ref != "" || subdir != "" {
|
|
u += "#" + gitRef.Ref
|
|
if subdir != "" {
|
|
u += ":" + subdir
|
|
}
|
|
}
|
|
return u, true
|
|
}
|