package client

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/containerd/containerd/v2/core/content"
	"github.com/containerd/containerd/v2/plugins/content/local"
	intoto "github.com/in-toto/in-toto-golang/in_toto"
	"github.com/moby/buildkit/client/llb"
	"github.com/moby/buildkit/client/llb/sourceresolver"
	gateway "github.com/moby/buildkit/frontend/gateway/client"
	provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
	"github.com/moby/buildkit/solver/pb"
	"github.com/moby/buildkit/util/contentutil"
	"github.com/moby/buildkit/util/gitutil/gitobject"
	"github.com/moby/buildkit/util/testutil/integration"
	"github.com/moby/buildkit/util/testutil/workers"
	digest "github.com/opencontainers/go-digest"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/require"
)

// testGitBundleRoundTrip exercises the bundle round-trip end to end:
//
//  1. Seed a git repo served over HTTP.
//  2. Build 1 uses GitCheckoutBundle() to export a single-file git bundle.
//  3. Build 2 imports that bundle via GitBundleURL("oci-layout+blob://...")
//     against a client-side OCI layout store, once with the sha pinned as ref
//     and once with a symbolic ref ("master"). Both checkouts must succeed
//     and produce a worktree matching Build 0 (plain git clone).
//  4. A KeepGitDir subtest exercises llb.KeepGitDir() + bundle and asserts
//     no fake refs/buildkit/* namespace leaks into the checkout's .git dir.
//  5. A provenance subtest enables attest:provenance and asserts both the
//     git-commit material and the bundle-blob material are recorded.
func testGitBundleRoundTrip(t *testing.T, sb integration.Sandbox) {
	workers.CheckFeatureCompat(t, sb, workers.FeatureOCILayout)
	integration.SkipOnPlatform(t, "windows")
	ctx := sb.Context()
	c, err := New(ctx, sb.Address())
	require.NoError(t, err)
	defer c.Close()

	gitDir := t.TempDir()
	err = runInDir(gitDir,
		"git init -b master",
		"git config --local user.email test@example.com",
		"git config --local user.name test",
		"echo hello > file.txt",
		"git add file.txt",
		"git commit -m initial",
		"git update-server-info",
	)
	require.NoError(t, err)

	cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "HEAD")
	cmd.Dir = gitDir
	out, err := cmd.Output()
	require.NoError(t, err)
	headSha := strings.TrimSpace(string(out))

	server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
	defer server.Close()
	repoURL := server.URL + "/.git"

	// Build 1: export a bundle via GitCheckoutBundle.
	bundleDir := t.TempDir()
	st := llb.Git(repoURL, "master", llb.GitChecksum(headSha), llb.GitCheckoutBundle())
	def, err := st.Marshal(ctx)
	require.NoError(t, err)

	_, err = c.Solve(ctx, def, SolveOpt{
		Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: bundleDir}},
	}, nil)
	require.NoError(t, err)

	bundleBytes, err := os.ReadFile(filepath.Join(bundleDir, "bundle"))
	require.NoError(t, err)
	require.NotEmpty(t, bundleBytes)
	bundleDgst := digest.FromBytes(bundleBytes)

	// Sanity-check the bundle locally: it must contain the commit we pinned,
	// landing in the natural ref namespace (refs/heads/master).
	localBare := t.TempDir()
	err = runInDir(localBare, "git init --bare")
	require.NoError(t, err)
	bundleRoot := t.TempDir()
	root, err := os.OpenRoot(bundleRoot)
	require.NoError(t, err)
	require.NoError(t, root.WriteFile("bundle.pack", bundleBytes, 0644))
	require.NoError(t, root.Close())
	bundlePath := filepath.Join(bundleRoot, "bundle.pack")
	err = runInDir(localBare, fmt.Sprintf("git fetch %s +refs/*:refs/*", bundlePath))
	require.NoError(t, err)
	//nolint:gosec // Test-controlled temp dir and commit SHA are not attacker input.
	cmd = exec.CommandContext(t.Context(), "git", "--git-dir="+localBare, "cat-file", "-e", headSha+"^{commit}")
	require.NoError(t, cmd.Run(), "bundle does not contain expected commit")
	//nolint:gosec // Test-controlled temp dir is not attacker input.
	cmd = exec.CommandContext(t.Context(), "git", "--git-dir="+localBare, "rev-parse", "--verify", "refs/heads/master")
	masterOut, err := cmd.CombinedOutput()
	require.NoError(t, err, "bundle should carry user's ref refs/heads/master; output=%q", string(masterOut))
	require.Equal(t, headSha, strings.TrimSpace(string(masterOut)))

	// Stage the bundle in a client-side OCI layout store so Build 2 can
	// resolve it through an oci-layout+blob:// locator.
	storeDir := t.TempDir()
	store, err := local.NewStore(storeDir)
	require.NoError(t, err)
	err = content.WriteBlob(ctx, store, "bundle-"+bundleDgst.String(), bytes.NewReader(bundleBytes),
		ocispecs.Descriptor{Digest: bundleDgst, Size: int64(len(bundleBytes))})
	require.NoError(t, err)

	csID := "git-bundle-oci-store"
	bundleLocator := fmt.Sprintf("oci-layout+blob://not/real@%s", bundleDgst.String())

	cases := []struct {
		name string
		ref  string // value passed as the fragment/ref to llb.Git
	}{
		{"sha-ref", headSha},
		{"symbolic-ref", "master"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			destDir := t.TempDir()
			st := llb.Git(
				repoURL,
				tc.ref,
				llb.GitChecksum(headSha),
				llb.GitBundleURL(bundleLocator, llb.GitBundleOCIStore("", csID)),
			)
			def, err := st.Marshal(sb.Context())
			require.NoError(t, err)

			_, err = c.Solve(sb.Context(), def, SolveOpt{
				Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
				OCIStores: map[string]content.Store{
					csID: store,
				},
			}, nil)
			require.NoError(t, err)

			dt, err := os.ReadFile(filepath.Join(destDir, "file.txt"))
			require.NoError(t, err)
			require.Equal(t, "hello\n", string(dt))
		})
	}

	// KeepGitDir + bundle: the checkout's .git must not contain any leaked
	// refs/buildkit/* namespace. `.git/refs/heads/*` and `.git/refs/tags/*`
	// are populated by the plain-git checkout path (it runs its own fetch
	// into a freshly inited repo), so we only assert no refs/buildkit/
	// leak — the whole point of Item A is that the bundle no longer uses
	// that namespace.
	t.Run("keepgitdir-no-namespace-leak", func(t *testing.T) {
		destDir := t.TempDir()
		st := llb.Git(
			repoURL,
			"master",
			llb.GitChecksum(headSha),
			llb.GitBundleURL(bundleLocator, llb.GitBundleOCIStore("", csID)),
			llb.KeepGitDir(),
		)
		def, err := st.Marshal(sb.Context())
		require.NoError(t, err)

		_, err = c.Solve(sb.Context(), def, SolveOpt{
			Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
			OCIStores: map[string]content.Store{
				csID: store,
			},
		}, nil)
		require.NoError(t, err)

		// .git dir should be present.
		st2, err := os.Stat(filepath.Join(destDir, ".git"))
		require.NoError(t, err)
		require.True(t, st2.IsDir(), ".git should be a directory")

		// No fake internal namespace anywhere under the checkout's .git.
		leakRoot := filepath.Join(destDir, ".git", "refs", "buildkit")
		_, err = os.Stat(leakRoot)
		require.True(t, os.IsNotExist(err), ".git/refs/buildkit should not exist: %v", err)

		// Also scan packed-refs, which is where cloned/fetched refs may
		// live on disk after git gc / pack-refs.
		if pr, err := os.ReadFile(filepath.Join(destDir, ".git", "packed-refs")); err == nil {
			require.NotContains(t, string(pr), "refs/buildkit/",
				"packed-refs should not carry refs/buildkit/* entries")
		}

		// The user's ref name must survive into .git: passing "master" to
		// llb.Git should produce a checkout whose .git carries
		// refs/heads/master, either loose or packed.
		//nolint:gosec // Test-controlled export dir is not attacker input.
		cmd := exec.CommandContext(t.Context(), "git",
			"--git-dir="+filepath.Join(destDir, ".git"),
			"rev-parse", "--verify", "refs/heads/master")
		refOut, err := cmd.CombinedOutput()
		require.NoError(t, err, ".git should carry refs/heads/master; output=%q", string(refOut))
		require.Equal(t, headSha, strings.TrimSpace(string(refOut)))

		// HEAD must point at the pinned commit.
		//nolint:gosec // Test-controlled export dir is not attacker input.
		cmd = exec.CommandContext(t.Context(), "git",
			"--git-dir="+filepath.Join(destDir, ".git"),
			"rev-parse", "HEAD")
		gotOut, err := cmd.CombinedOutput()
		require.NoError(t, err, "output=%q", string(gotOut))
		require.Equal(t, headSha, strings.TrimSpace(string(gotOut)))

		// file.txt content matches.
		dt, err := os.ReadFile(filepath.Join(destDir, "file.txt"))
		require.NoError(t, err)
		require.Equal(t, "hello\n", string(dt))
	})

	// Provenance subtest: assert that the git-commit material and the
	// bundle-blob material are both recorded.
	t.Run("provenance", func(t *testing.T) {
		workers.CheckFeatureCompat(t, sb, workers.FeatureProvenance)
		destDir := t.TempDir()
		st := llb.Scratch().File(
			llb.Copy(
				llb.Git(
					repoURL,
					"master",
					llb.GitChecksum(headSha),
					llb.GitBundleURL(bundleLocator, llb.GitBundleOCIStore("", csID)),
				),
				"file.txt",
				"file.txt",
			),
		)
		def, err := st.Marshal(sb.Context())
		require.NoError(t, err)

		_, err = c.Solve(sb.Context(), def, SolveOpt{
			FrontendAttrs: map[string]string{
				"attest:provenance": "",
			},
			Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
			OCIStores: map[string]content.Store{
				csID: store,
			},
		}, nil)
		require.NoError(t, err)

		provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
		require.NoError(t, err)

		var stmt struct {
			intoto.StatementHeader
			Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
		}
		require.NoError(t, json.Unmarshal(provDt, &stmt))

		// Bundle-backed git sources emit two materials: the git source
		// at its raw repository URL (same shape as a non-bundle git
		// source), and the bundle blob as a purl with a vcs_url
		// qualifier linking back to the git URL.
		expectedGitURI := repoURL + "#master"
		var foundCommit, foundBundle bool
		for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
			if m.URI == expectedGitURI && m.Digest["sha1"] == headSha {
				foundCommit = true
			}
			if strings.HasPrefix(m.URI, "pkg:oci/") && strings.Contains(m.URI, "ref_type=bundle") && strings.Contains(m.URI, "vcs_url=") && m.Digest["sha256"] == bundleDgst.Hex() {
				foundBundle = true
			}
		}
		require.True(t, foundCommit, "expected git commit material %q with sha1=%s in %+v", expectedGitURI, headSha, stmt.Predicate.BuildDefinition.ResolvedDependencies)
		require.True(t, foundBundle, "expected bundle blob material (pkg:oci/...?ref_type=bundle) with sha256=%s in %+v", bundleDgst.Hex(), stmt.Predicate.BuildDefinition.ResolvedDependencies)
	})

	checkAllReleasable(t, c, sb, false)
}

// testGitBundleRoundTripRegistry mirrors testGitBundleRoundTrip but resolves
// the bundle blob through a registry using the docker-image+blob:// scheme.
// The bundle bytes are uploaded directly to the sandbox registry by digest
// and then re-imported in Build 2.
func testGitBundleRoundTripRegistry(t *testing.T, sb integration.Sandbox) {
	workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
	integration.SkipOnPlatform(t, "windows")
	requiresLinux(t)
	ctx := sb.Context()
	c, err := New(ctx, sb.Address())
	require.NoError(t, err)
	defer c.Close()

	registry, err := sb.NewRegistry()
	if errors.Is(err, integration.ErrRequirements) {
		t.Skip(err.Error())
	}
	require.NoError(t, err)

	gitDir := t.TempDir()
	err = runInDir(gitDir,
		"git init -b master",
		"git config --local user.email test@example.com",
		"git config --local user.name test",
		"echo hello > file.txt",
		"git add file.txt",
		"git commit -m initial",
		"git update-server-info",
	)
	require.NoError(t, err)

	cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "HEAD")
	cmd.Dir = gitDir
	out, err := cmd.Output()
	require.NoError(t, err)
	headSha := strings.TrimSpace(string(out))

	server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
	defer server.Close()
	repoURL := server.URL + "/.git"

	// Build 1: export a bundle via GitCheckoutBundle.
	bundleDir := t.TempDir()
	st := llb.Git(repoURL, "master", llb.GitChecksum(headSha), llb.GitCheckoutBundle())
	def, err := st.Marshal(ctx)
	require.NoError(t, err)

	_, err = c.Solve(ctx, def, SolveOpt{
		Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: bundleDir}},
	}, nil)
	require.NoError(t, err)

	bundleBytes, err := os.ReadFile(filepath.Join(bundleDir, "bundle"))
	require.NoError(t, err)
	require.NotEmpty(t, bundleBytes)
	bundleDgst := digest.FromBytes(bundleBytes)

	// Push the bundle as a raw blob into the sandbox registry under a
	// repository name. The docker registry accepts arbitrary blobs by
	// digest; the repository exists implicitly once any blob is uploaded
	// under its name.
	bundleRepoRef := registry + "/foo/bundle@" + bundleDgst.String()
	ingester, err := contentutil.IngesterFromRef(sb.Context(), bundleRepoRef)
	require.NoError(t, err)
	err = content.WriteBlob(ctx, ingester, "bundle-"+bundleDgst.String(), bytes.NewReader(bundleBytes),
		ocispecs.Descriptor{Digest: bundleDgst, Size: int64(len(bundleBytes))})
	require.NoError(t, err)

	bundleLocator := "docker-image+blob://" + registry + "/foo/bundle@" + bundleDgst.String()

	t.Run("checkout", func(t *testing.T) {
		destDir := t.TempDir()
		st := llb.Git(
			repoURL,
			"master",
			llb.GitChecksum(headSha),
			llb.GitBundleURL(bundleLocator),
		)
		def, err := st.Marshal(sb.Context())
		require.NoError(t, err)

		_, err = c.Solve(sb.Context(), def, SolveOpt{
			Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
		}, nil)
		require.NoError(t, err)

		dt, err := os.ReadFile(filepath.Join(destDir, "file.txt"))
		require.NoError(t, err)
		require.Equal(t, "hello\n", string(dt))
	})

	t.Run("provenance", func(t *testing.T) {
		workers.CheckFeatureCompat(t, sb, workers.FeatureProvenance)
		destDir := t.TempDir()
		st := llb.Scratch().File(
			llb.Copy(
				llb.Git(
					repoURL,
					"master",
					llb.GitChecksum(headSha),
					llb.GitBundleURL(bundleLocator),
				),
				"file.txt",
				"file.txt",
			),
		)
		def, err := st.Marshal(sb.Context())
		require.NoError(t, err)

		_, err = c.Solve(sb.Context(), def, SolveOpt{
			FrontendAttrs: map[string]string{
				"attest:provenance": "",
			},
			Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
		}, nil)
		require.NoError(t, err)

		provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
		require.NoError(t, err)

		var stmt struct {
			intoto.StatementHeader
			Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
		}
		require.NoError(t, json.Unmarshal(provDt, &stmt))

		expectedGitURI := repoURL + "#master"
		var foundCommit, foundBundle bool
		for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
			if m.URI == expectedGitURI && m.Digest["sha1"] == headSha {
				foundCommit = true
			}
			if strings.HasPrefix(m.URI, "pkg:docker/") && strings.Contains(m.URI, "ref_type=bundle") && strings.Contains(m.URI, "vcs_url=") && m.Digest["sha256"] == bundleDgst.Hex() {
				foundBundle = true
			}
		}
		require.True(t, foundCommit, "expected git commit material %q with sha1=%s in %+v", expectedGitURI, headSha, stmt.Predicate.BuildDefinition.ResolvedDependencies)
		require.True(t, foundBundle, "expected bundle blob material (pkg:docker/...?ref_type=bundle) with sha256=%s in %+v", bundleDgst.Hex(), stmt.Predicate.BuildDefinition.ResolvedDependencies)
	})

	checkAllReleasable(t, c, sb, false)
}

func testGitResolveMutatedSource(t *testing.T, sb integration.Sandbox) {
	integration.SkipOnPlatform(t, "windows")
	ctx := sb.Context()
	c, err := New(ctx, sb.Address())
	require.NoError(t, err)
	defer c.Close()

	gitDir := t.TempDir()
	gitCommands := []string{
		"git init",
		"git config --local user.email test",
		"git config --local user.name test",
		"echo a > a",
		"git add a",
		"git commit -m a",
		"git tag -a v0.1 -m v0.1",
		"echo b > b",
		"git add b",
		"git commit -m b",
		"git checkout -B v2",
		"git update-server-info",
	}
	err = runInDir(gitDir, gitCommands...)
	require.NoError(t, err)

	cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "v0.1")
	cmd.Dir = gitDir
	out, err := cmd.Output()
	require.NoError(t, err)
	commitTag := strings.TrimSpace(string(out))

	cmd = exec.CommandContext(t.Context(), "git", "rev-parse", "v0.1^{commit}")
	cmd.Dir = gitDir
	out, err = cmd.Output()
	require.NoError(t, err)
	commitTagCommit := strings.TrimSpace(string(out))

	server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
	defer server.Close()

	dest := t.TempDir()

	_, err = c.Build(ctx, SolveOpt{
		Exports: []ExportEntry{
			{
				Type:      ExporterLocal,
				OutputDir: dest,
			},
		},
	}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
		id := "git://" + strings.TrimPrefix(server.URL, "http://") + "/.git#v0.1"
		md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
			Identifier: id,
			Attrs: map[string]string{
				"git.fullurl": server.URL + "/.git",
			},
		}, sourceresolver.Opt{})
		if err != nil {
			return nil, err
		}
		require.NotNil(t, md.Git)
		require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
		require.Equal(t, commitTag, md.Git.Checksum)
		require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
		require.Equal(t, id, md.Op.Identifier)
		require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])

		// update the tag to point to a different commit
		err = runInDir(gitDir, []string{
			"git tag -f v0.1",
			"git update-server-info",
		}...)
		require.NoError(t, err)

		md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
			Identifier: id,
			Attrs: map[string]string{
				"git.fullurl": server.URL + "/.git",
			},
		}, sourceresolver.Opt{})
		if err != nil {
			return nil, err
		}
		require.NotNil(t, md.Git)
		require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
		require.Equal(t, commitTag, md.Git.Checksum)
		require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
		require.Equal(t, id, md.Op.Identifier)
		require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])

		st := llb.Git(server.URL+"/.git", "", llb.GitRef("v0.1"))
		def, err := st.Marshal(sb.Context())
		if err != nil {
			return nil, err
		}
		return c.Solve(ctx, gateway.SolveRequest{
			Definition: def.ToPB(),
		})
	}, nil)
	require.NoError(t, err)

	_, err = os.ReadFile(filepath.Join(dest, "b"))
	require.Error(t, err)
	require.True(t, os.IsNotExist(err), "expected file b to not exist")

	dt, err := os.ReadFile(filepath.Join(dest, "a"))
	require.NoError(t, err)
	require.Equal(t, "a\n", string(dt))

	checkAllReleasable(t, c, sb, false)
}

func testGitResolveSourceMetadata(t *testing.T, sb integration.Sandbox) {
	ctx := sb.Context()
	c, err := New(ctx, sb.Address())
	require.NoError(t, err)
	defer c.Close()

	gitDir := t.TempDir()
	gitCommands := []string{
		"git init",
		"git config --local user.email test@example.com",
		"git config --local user.name test",
		"touch a",
		"git add a",
		"git commit -m msg",
		"git tag -a v0.1 -m v0.1release",
		"echo b > b",
		"git add b",
		"git commit -m b",
		"git checkout -B v2",
		"git update-server-info",
	}
	err = runInDir(gitDir, gitCommands...)
	require.NoError(t, err)

	cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "HEAD")
	cmd.Dir = gitDir
	out, err := cmd.Output()
	require.NoError(t, err)
	commitHEAD := strings.TrimSpace(string(out))

	cmd = exec.CommandContext(t.Context(), "git", "rev-parse", "v0.1")
	cmd.Dir = gitDir
	out, err = cmd.Output()
	require.NoError(t, err)
	commitTag := strings.TrimSpace(string(out))

	cmd = exec.CommandContext(t.Context(), "git", "rev-parse", "v0.1^{commit}")
	cmd.Dir = gitDir
	out, err = cmd.Output()
	require.NoError(t, err)
	commitTagCommit := strings.TrimSpace(string(out))

	server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
	defer server.Close()

	_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
		id := "git://" + strings.TrimPrefix(server.URL, "http://") + "/.git"
		md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
			Identifier: id,
			Attrs: map[string]string{
				"git.fullurl": server.URL + "/.git",
			},
		}, sourceresolver.Opt{})
		if err != nil {
			return nil, err
		}
		require.NotNil(t, md.Git)
		require.Equal(t, "refs/heads/v2", md.Git.Ref) // default to branch head
		require.Equal(t, commitHEAD, md.Git.Checksum)
		require.Equal(t, "", md.Git.CommitChecksum) // not annotated tag
		require.Equal(t, id, md.Op.Identifier)
		require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
		require.Nil(t, md.Git.CommitObject)
		require.Nil(t, md.Git.TagObject)

		id += "#v0.1"
		md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
			Identifier: id,
			Attrs: map[string]string{
				"git.fullurl": server.URL + "/.git",
			},
		}, sourceresolver.Opt{})
		if err != nil {
			return nil, err
		}
		require.NotNil(t, md.Git)
		require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
		require.Equal(t, commitTag, md.Git.Checksum) // annotated tag
		require.Equal(t, commitTagCommit, md.Git.CommitChecksum)

		require.Equal(t, id, md.Op.Identifier)
		require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
		require.Nil(t, md.Git.CommitObject)
		require.Nil(t, md.Git.TagObject)

		md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
			Identifier: id,
			Attrs: map[string]string{
				"git.fullurl": server.URL + "/.git",
			},
		}, sourceresolver.Opt{
			GitOpt: &sourceresolver.ResolveGitOpt{
				ReturnObject: true,
			},
		})
		if err != nil {
			return nil, err
		}
		require.NotNil(t, md.Git)
		require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
		require.Equal(t, commitTag, md.Git.Checksum) // annotated tag
		require.Equal(t, commitTagCommit, md.Git.CommitChecksum)

		require.Equal(t, id, md.Op.Identifier)
		require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
		require.NotNil(t, md.Git.CommitObject)
		require.NotNil(t, md.Git.TagObject)

		commitObj, err := gitobject.Parse(md.Git.CommitObject)
		require.NoError(t, err)
		require.NoError(t, commitObj.VerifyChecksum(md.Git.CommitChecksum))

		commit, err := commitObj.ToCommit()
		require.NoError(t, err)
		require.Equal(t, "msg", commit.Message)
		require.Equal(t, "test", commit.Author.Name)
		require.Equal(t, "test@example.com", commit.Author.Email)
		require.Equal(t, "test", commit.Committer.Name)
		require.Equal(t, "test@example.com", commit.Committer.Email)
		commitTime := commit.Committer.When
		require.NotNil(t, commitTime)
		require.WithinDuration(t, time.Now(), *commitTime, 2*time.Minute)

		tagObj, err := gitobject.Parse(md.Git.TagObject)
		require.NoError(t, err)
		require.NoError(t, tagObj.VerifyChecksum(md.Git.Checksum))

		tag, err := tagObj.ToTag()
		require.NoError(t, err)
		require.Equal(t, "v0.1release", tag.Message)
		require.Equal(t, "v0.1", tag.Tag)
		require.Equal(t, "test", tag.Tagger.Name)
		require.Equal(t, "test@example.com", tag.Tagger.Email)
		tagTime := tag.Tagger.When
		require.NotNil(t, tagTime)
		require.WithinDuration(t, time.Now(), *tagTime, 2*time.Minute)
		return nil, nil
	}, nil)
	require.NoError(t, err)
}

func runInDir(dir string, cmds ...string) error {
	return runInDirEnv(dir, nil, cmds...)
}

func runInDirEnv(dir string, env []string, cmds ...string) error {
	for _, args := range cmds {
		var cmd *exec.Cmd
		if runtime.GOOS == "windows" {
			cmd = exec.CommandContext(context.TODO(), "powershell", "-command", args)
		} else {
			cmd = exec.CommandContext(context.TODO(), "sh", "-c", args)
		}
		cmd.Env = append(os.Environ(), env...)
		cmd.Dir = dir
		if err := cmd.Run(); err != nil {
			return errors.Wrapf(err, "error running %v", args)
		}
	}
	return nil
}
