diff --git a/add.go b/add.go index f5a54a34d19..c523dbf28b0 100644 --- a/add.go +++ b/add.go @@ -20,6 +20,7 @@ import ( "github.com/containers/buildah/copier" "github.com/containers/buildah/define" + "github.com/containers/buildah/internal/tmpdir" "github.com/containers/buildah/pkg/chrootuser" "github.com/containers/common/pkg/retry" "github.com/containers/image/v5/pkg/tlsclientconfig" @@ -93,9 +94,14 @@ type AddAndCopyOptions struct { RetryDelay time.Duration } +// sourceIsGit returns true if "source" is a git location. +func sourceIsGit(source string) bool { + return strings.HasPrefix(source, "git://") || strings.Contains(source, ".git") +} + // sourceIsRemote returns true if "source" is a remote location. func sourceIsRemote(source string) bool { - return strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") + return (strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://")) && !strings.Contains(source, ".git") } // getURL writes a tar archive containing the named content @@ -274,7 +280,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption } // Figure out what sorts of sources we have. - var localSources, remoteSources []string + var localSources, remoteSources, gitSources []string for i, src := range sources { if src == "" { return errors.New("empty source location") @@ -283,12 +289,22 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption remoteSources = append(remoteSources, src) continue } + if sourceIsGit(src) { + gitSources = append(gitSources, src) + continue + } if !filepath.IsAbs(src) && options.ContextDir == "" { sources[i] = filepath.Join(currentDir, src) } localSources = append(localSources, sources[i]) } + // Treat git sources as a subset of remote sources + // differentiating only in how we fetch the two later on. + if len(gitSources) > 0 { + remoteSources = append(remoteSources, gitSources...) + } + // Check how many items our local source specs matched. Each spec // should have matched at least one item, otherwise we consider it an // error. @@ -320,7 +336,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption } numLocalSourceItems += len(localSourceStat.Globbed) } - if numLocalSourceItems+len(remoteSources) == 0 { + if numLocalSourceItems+len(remoteSources)+len(gitSources) == 0 { return fmt.Errorf("no sources %v found: %w", sources, syscall.ENOENT) } @@ -377,6 +393,9 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption destCanBeFile = true } } + if len(gitSources) > 0 { + destMustBeDirectory = true + } } // We care if the destination either doesn't exist, or exists and is a @@ -448,7 +467,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption var multiErr *multierror.Error var getErr, closeErr, renameErr, putErr error var wg sync.WaitGroup - if sourceIsRemote(src) { + if sourceIsRemote(src) || sourceIsGit(src) { pipeReader, pipeWriter := io.Pipe() var srcDigest digest.Digest if options.Checksum != "" { @@ -457,7 +476,9 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption return fmt.Errorf("invalid checksum flag: %w", err) } } + wg.Add(1) + go func() { getErr = retry.IfNecessary(context.TODO(), func() error { return getURL(src, chownFiles, mountPoint, renameTarget, pipeWriter, chmodDirsFiles, srcDigest, options.CertPath, options.InsecureSkipTLSVerify) @@ -468,6 +489,45 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption pipeWriter.Close() wg.Done() }() + + if sourceIsGit(src) { + go func() { + var cloneDir string + cloneDir, _, getErr = define.TempDirForURL(tmpdir.GetTempDir(), "", src) + + renamedItems := 0 + writer := io.WriteCloser(pipeWriter) + writer = newTarFilterer(writer, func(hdr *tar.Header) (bool, bool, io.Reader) { + return false, false, nil + }) + getOptions := copier.GetOptions{ + UIDMap: srcUIDMap, + GIDMap: srcGIDMap, + Excludes: options.Excludes, + ExpandArchives: extract, + ChownDirs: chownDirs, + ChmodDirs: chmodDirsFiles, + ChownFiles: chownFiles, + ChmodFiles: chmodDirsFiles, + StripSetuidBit: options.StripSetuidBit, + StripSetgidBit: options.StripSetgidBit, + StripStickyBit: options.StripStickyBit, + } + getErr = copier.Get(cloneDir, cloneDir, getOptions, []string{"."}, writer) + closeErr = writer.Close() + if renameTarget != "" && renamedItems > 1 { + renameErr = fmt.Errorf("internal error: renamed %d items when we expected to only rename 1", renamedItems) + } + wg.Done() + }() + } else { + go func() { + getErr = getURL(src, chownFiles, mountPoint, renameTarget, pipeWriter, chmodDirsFiles, srcDigest) + pipeWriter.Close() + wg.Done() + }() + } + wg.Add(1) go func() { b.ContentDigester.Start("") diff --git a/define/types.go b/define/types.go index bfe075bbfa7..3f53c1c1426 100644 --- a/define/types.go +++ b/define/types.go @@ -254,9 +254,16 @@ func parseGitBuildContext(url string) (string, string, string) { return gitBranchPart[0], gitSubdir, gitBranch } +func isGitTag(remote, ref, dir string) (bool, error) { + if _, err := exec.Command("git", "ls-remote", "--exit-code", remote, ref).Output(); err != nil { + return true, nil + } + return false, nil +} + func cloneToDirectory(url, dir string) ([]byte, string, error) { var cmd *exec.Cmd - gitRepo, gitSubdir, gitBranch := parseGitBuildContext(url) + gitRepo, gitSubdir, gitRef := parseGitBuildContext(url) // init repo cmd = exec.Command("git", "init", dir) combinedOutput, err := cmd.CombinedOutput() @@ -270,27 +277,23 @@ func cloneToDirectory(url, dir string) ([]byte, string, error) { if err != nil { return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git remote add`: %w", err) } - // fetch required branch or commit and perform checkout - // Always default to `HEAD` if nothing specified - fetch := "HEAD" - if gitBranch != "" { - fetch = gitBranch + + if gitRef != "" { + if ok, _ := isGitTag(url, gitRef, dir); ok { + gitRef += ":refs/tags/" + gitRef + } } - logrus.Debugf("fetching repo %q and branch (or commit ID) %q to %q", gitRepo, fetch, dir) - cmd = exec.Command("git", "fetch", "--depth=1", "origin", "--", fetch) + + logrus.Debugf("fetching repo %q and branch (or commit ID) %q to %q", gitRepo, gitRef, dir) + args := []string{"fetch", "-u", "--depth=1", "origin", "--", gitRef} + cmd = exec.Command("git", args...) cmd.Dir = dir combinedOutput, err = cmd.CombinedOutput() if err != nil { return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git fetch`: %w", err) } - if fetch == "HEAD" { - // We fetched default branch therefore - // we don't have any valid `branch` or - // `commit` name hence checkout detached - // `FETCH_HEAD` - fetch = "FETCH_HEAD" - } - cmd = exec.Command("git", "checkout", fetch) + + cmd = exec.Command("git", "checkout", "FETCH_HEAD") cmd.Dir = dir combinedOutput, err = cmd.CombinedOutput() if err != nil {