diff --git a/internal/cli/banger.go b/internal/cli/banger.go index 07d71a0..6c75720 100644 --- a/internal/cli/banger.go +++ b/internal/cli/banger.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net" + "net/url" "os" "os/exec" "path/filepath" @@ -93,13 +94,14 @@ var ( guestDialFunc = func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error) { return guest.Dial(ctx, address, privateKeyPath) } - cwdFunc = os.Getwd + prepareVMRunRepoCopyFunc = prepareVMRunRepoCopy + cwdFunc = os.Getwd ) type vmRunGuestClient interface { Close() error - UploadFile(ctx context.Context, remotePath string, mode os.FileMode, data []byte, logWriter io.Writer) error RunScript(ctx context.Context, script string, logWriter io.Writer) error + StreamTar(ctx context.Context, sourceDir, remoteCommand string, logWriter io.Writer) error StreamTarEntries(ctx context.Context, sourceDir string, entries []string, remoteCommand string, logWriter io.Writer) error } @@ -111,12 +113,13 @@ type vmRunRepoSpec struct { CurrentBranch string BranchName string BaseCommit string + OriginURL string GitUserName string GitUserEmail string OverlayPaths []string } -const vmRunGuestBundlePath = "/tmp/banger-vm-run.bundle" +const vmRunShallowFetchDepth = 10 func NewBangerCommand() *cobra.Command { root := &cobra.Command{ @@ -1454,6 +1457,10 @@ func inspectVMRunRepo(ctx context.Context, rawPath, branchName, fromRef string) if err != nil { return vmRunRepoSpec{}, fmt.Errorf("resolve git user.email for %s: %w", repoRoot, err) } + originURL, err := gitResolvedConfigValue(ctx, repoRoot, "remote.origin.url") + if err != nil { + return vmRunRepoSpec{}, fmt.Errorf("resolve origin url for %s: %w", repoRoot, err) + } overlayPaths, err := listVMRunOverlayPaths(ctx, repoRoot) if err != nil { @@ -1468,6 +1475,7 @@ func inspectVMRunRepo(ctx context.Context, rawPath, branchName, fromRef string) CurrentBranch: currentBranch, BranchName: branchName, BaseCommit: baseCommit, + OriginURL: originURL, GitUserName: gitUserName, GitUserEmail: gitUserEmail, OverlayPaths: overlayPaths, @@ -1583,6 +1591,7 @@ func parseNullSeparatedOutput(output []byte) []string { } func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, stdin io.Reader, stdout, stderr io.Writer, params api.VMCreateParams, spec vmRunRepoSpec) error { + progress := newVMRunProgressRenderer(stderr) vm, err := runVMCreate(ctx, socketPath, stderr, params) if err != nil { return err @@ -1592,6 +1601,7 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st vmRef = shortID(vm.ID) } sshAddress := net.JoinHostPort(vm.Runtime.GuestIP, "22") + progress.render("waiting for guest ssh") if err := guestWaitForSSHFunc(ctx, sshAddress, cfg.SSHKeyPath, 250*time.Millisecond); err != nil { return fmt.Errorf("vm %q is running but guest ssh is unavailable: %w", vmRef, err) } @@ -1600,71 +1610,112 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st return fmt.Errorf("vm %q is running but guest ssh is unavailable: %w", vmRef, err) } defer client.Close() - if err := importVMRunRepoToGuest(ctx, client, spec); err != nil { + if err := importVMRunRepoToGuest(ctx, client, spec, progress); err != nil { return fmt.Errorf("vm %q is running but repo import failed: %w", vmRef, err) } + progress.render("attaching opencode") if err := runVMRunAttach(ctx, stdin, stdout, stderr, vm.Runtime.GuestIP, vmRunGuestDir(spec.RepoName)); err != nil { return fmt.Errorf("vm %q is running but opencode attach failed: %w", vmRef, err) } return nil } -func importVMRunRepoToGuest(ctx context.Context, client vmRunGuestClient, spec vmRunRepoSpec) error { - bundleData, err := createVMRunBundle(ctx, spec) +func importVMRunRepoToGuest(ctx context.Context, client vmRunGuestClient, spec vmRunRepoSpec, progress *vmRunProgressRenderer) error { + if progress != nil { + progress.render("preparing shallow repo") + } + repoCopyDir, cleanup, err := prepareVMRunRepoCopyFunc(ctx, spec) if err != nil { return err } - var uploadLog bytes.Buffer - if err := client.UploadFile(ctx, vmRunGuestBundlePath, 0o600, bundleData, &uploadLog); err != nil { - return formatVMRunStepError("upload git bundle", err, uploadLog.String()) + defer cleanup() + if progress != nil { + progress.render("copying repo metadata to guest") + } + var copyLog bytes.Buffer + remoteCommand := fmt.Sprintf("rm -rf %s && mkdir -p %s && tar -o -C %s --strip-components=1 -xf -", shellQuote(vmRunGuestDir(spec.RepoName)), shellQuote(vmRunGuestDir(spec.RepoName)), shellQuote(vmRunGuestDir(spec.RepoName))) + if err := client.StreamTar(ctx, repoCopyDir, remoteCommand, ©Log); err != nil { + return formatVMRunStepError("copy guest git metadata", err, copyLog.String()) + } + if progress != nil { + progress.render("preparing guest checkout") } var scriptLog bytes.Buffer - if err := client.RunScript(ctx, vmRunCloneScript(spec), &scriptLog); err != nil { + if err := client.RunScript(ctx, vmRunCheckoutScript(spec), &scriptLog); err != nil { return formatVMRunStepError("prepare guest checkout", err, scriptLog.String()) } + if progress != nil { + progress.render("overlaying host working tree") + } var overlayLog bytes.Buffer - remoteCommand := fmt.Sprintf("tar -o -C %s --strip-components=1 -xf -", shellQuote(vmRunGuestDir(spec.RepoName))) + remoteCommand = fmt.Sprintf("tar -o -C %s --strip-components=1 -xf -", shellQuote(vmRunGuestDir(spec.RepoName))) if err := client.StreamTarEntries(ctx, spec.RepoRoot, spec.OverlayPaths, remoteCommand, &overlayLog); err != nil { return formatVMRunStepError("overlay host working tree", err, overlayLog.String()) } return nil } -func createVMRunBundle(ctx context.Context, spec vmRunRepoSpec) ([]byte, error) { - tempFile, err := os.CreateTemp("", "banger-vm-run-*.bundle") +func prepareVMRunRepoCopy(ctx context.Context, spec vmRunRepoSpec) (string, func(), error) { + tempRoot, err := os.MkdirTemp("", "banger-vm-run-*") if err != nil { - return nil, err + return "", nil, err } - tempPath := tempFile.Name() - if err := tempFile.Close(); err != nil { - _ = os.Remove(tempPath) - return nil, err + cleanup := func() { + _ = os.RemoveAll(tempRoot) } - defer os.Remove(tempPath) - - args := []string{"-C", spec.RepoRoot, "bundle", "create", tempPath, "--all"} - for _, rev := range uniqueNonEmptyStrings(spec.HeadCommit, spec.BaseCommit) { - args = append(args, rev) + repoCopyDir := filepath.Join(tempRoot, spec.RepoName) + cloneArgs := []string{"clone", "--no-checkout", "--depth", fmt.Sprintf("%d", vmRunShallowFetchDepth)} + if strings.TrimSpace(spec.CurrentBranch) != "" { + cloneArgs = append(cloneArgs, "--single-branch", "--branch", spec.CurrentBranch) } - if _, err := hostCommandOutputFunc(ctx, "git", args...); err != nil { - return nil, fmt.Errorf("create git bundle: %w", err) + cloneArgs = append(cloneArgs, gitFileURL(spec.RepoRoot), repoCopyDir) + if err := runHostCommand(ctx, "git", cloneArgs...); err != nil { + cleanup() + return "", nil, fmt.Errorf("clone shallow repo copy: %w", err) } - data, err := os.ReadFile(tempPath) - if err != nil { - return nil, fmt.Errorf("read git bundle: %w", err) + checkoutCommit := vmRunCheckoutCommit(spec) + if err := runHostCommand(ctx, "git", "-C", repoCopyDir, "cat-file", "-e", checkoutCommit+"^{commit}"); err != nil { + if err := runHostCommand(ctx, "git", "-C", repoCopyDir, "fetch", "--depth", fmt.Sprintf("%d", vmRunShallowFetchDepth), gitFileURL(spec.RepoRoot), checkoutCommit); err != nil { + cleanup() + return "", nil, fmt.Errorf("fetch shallow repo commit %s: %w", checkoutCommit, err) + } } - return data, nil + if strings.TrimSpace(spec.OriginURL) != "" { + if err := runHostCommand(ctx, "git", "-C", repoCopyDir, "remote", "set-url", "origin", spec.OriginURL); err != nil { + cleanup() + return "", nil, fmt.Errorf("set origin remote: %w", err) + } + } else { + if err := runHostCommand(ctx, "git", "-C", repoCopyDir, "remote", "remove", "origin"); err != nil { + cleanup() + return "", nil, fmt.Errorf("remove placeholder origin remote: %w", err) + } + } + return repoCopyDir, cleanup, nil } -func vmRunCloneScript(spec vmRunRepoSpec) string { +func vmRunCheckoutCommit(spec vmRunRepoSpec) string { + if strings.TrimSpace(spec.BranchName) != "" { + return spec.BaseCommit + } + return spec.HeadCommit +} + +func gitFileURL(path string) string { + return (&url.URL{Scheme: "file", Path: filepath.ToSlash(path)}).String() +} + +func runHostCommand(ctx context.Context, name string, args ...string) error { + _, err := hostCommandOutputFunc(ctx, name, args...) + return err +} + +func vmRunCheckoutScript(spec vmRunRepoSpec) string { guestDir := vmRunGuestDir(spec.RepoName) var script strings.Builder script.WriteString("set -euo pipefail\n") fmt.Fprintf(&script, "DIR=%s\n", shellQuote(guestDir)) - fmt.Fprintf(&script, "BUNDLE=%s\n", shellQuote(vmRunGuestBundlePath)) - script.WriteString("rm -rf \"$DIR\"\n") - script.WriteString("git clone \"$BUNDLE\" \"$DIR\"\n") - script.WriteString("rm -f \"$BUNDLE\"\n") + script.WriteString("git config --global --add safe.directory \"$DIR\"\n") switch { case strings.TrimSpace(spec.BranchName) != "": fmt.Fprintf(&script, "git -C \"$DIR\" checkout -B %s %s\n", shellQuote(spec.BranchName), shellQuote(spec.BaseCommit)) @@ -1674,7 +1725,6 @@ func vmRunCloneScript(spec vmRunRepoSpec) string { fmt.Fprintf(&script, "git -C \"$DIR\" checkout --detach %s\n", shellQuote(spec.HeadCommit)) } script.WriteString("find \"$DIR\" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +\n") - script.WriteString("git config --global --add safe.directory \"$DIR\"\n") if strings.TrimSpace(spec.GitUserName) != "" && strings.TrimSpace(spec.GitUserEmail) != "" { fmt.Fprintf(&script, "git -C \"$DIR\" config user.name %s\n", shellQuote(spec.GitUserName)) fmt.Fprintf(&script, "git -C \"$DIR\" config user.email %s\n", shellQuote(spec.GitUserEmail)) @@ -1706,21 +1756,37 @@ func formatVMRunStepError(action string, err error, log string) error { return fmt.Errorf("%s: %w: %s", action, err, log) } -func uniqueNonEmptyStrings(values ...string) []string { - unique := make([]string, 0, len(values)) - seen := make(map[string]struct{}, len(values)) - for _, value := range values { - value = strings.TrimSpace(value) - if value == "" { - continue - } - if _, ok := seen[value]; ok { - continue - } - seen[value] = struct{}{} - unique = append(unique, value) +type vmRunProgressRenderer struct { + out io.Writer + enabled bool + lastLine string +} + +func newVMRunProgressRenderer(out io.Writer) *vmRunProgressRenderer { + return &vmRunProgressRenderer{ + out: out, + enabled: out != nil, } - return unique +} + +func (r *vmRunProgressRenderer) render(detail string) { + if r == nil || !r.enabled { + return + } + line := formatVMRunProgress(detail) + if line == "" || line == r.lastLine { + return + } + r.lastLine = line + _, _ = fmt.Fprintln(r.out, line) +} + +func formatVMRunProgress(detail string) string { + detail = strings.TrimSpace(detail) + if detail == "" { + return "" + } + return "[vm run] " + detail } func shellQuote(value string) string { diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index aaf6d56..3f47921 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -477,6 +477,26 @@ func TestVMCreateProgressRendererSuppressesDuplicateLines(t *testing.T) { } } +func TestVMRunProgressRendererSuppressesDuplicateLines(t *testing.T) { + var stderr bytes.Buffer + renderer := newVMRunProgressRenderer(&stderr) + + renderer.render("waiting for guest ssh") + renderer.render("waiting for guest ssh") + renderer.render("overlaying host working tree") + + lines := strings.Split(strings.TrimSpace(stderr.String()), "\n") + if len(lines) != 2 { + t.Fatalf("rendered lines = %q, want 2 lines", stderr.String()) + } + if lines[0] != "[vm run] waiting for guest ssh" { + t.Fatalf("first line = %q", lines[0]) + } + if lines[1] != "[vm run] overlaying host working tree" { + t.Fatalf("second line = %q", lines[1]) + } +} + func TestVMSetParamsFromFlagsConflict(t *testing.T) { if _, err := vmSetParamsFromFlags("devbox", -1, -1, "", true, true); err == nil { t.Fatal("expected nat conflict error") @@ -923,6 +943,7 @@ func TestInspectVMRunRepoUsesRepoRootAndOverlayPaths(t *testing.T) { testRunGit(t, repoRoot, "config", "--global", "user.email", "global@example.com") testRunGit(t, repoRoot, "config", "--global", "user.name", "Global User") testRunGit(t, repoRoot, "init") + testRunGit(t, repoRoot, "remote", "add", "origin", "https://example.com/repo.git") testRunGit(t, repoRoot, "config", "user.email", "test@example.com") testRunGit(t, repoRoot, "config", "user.name", "Banger Test") @@ -972,6 +993,9 @@ func TestInspectVMRunRepoUsesRepoRootAndOverlayPaths(t *testing.T) { if spec.BaseCommit != spec.HeadCommit { t.Fatalf("BaseCommit = %q, want head %q", spec.BaseCommit, spec.HeadCommit) } + if spec.OriginURL != "https://example.com/repo.git" { + t.Fatalf("OriginURL = %q, want https://example.com/repo.git", spec.OriginURL) + } if spec.GitUserName != "Banger Test" { t.Fatalf("GitUserName = %q, want Banger Test", spec.GitUserName) } @@ -1018,13 +1042,14 @@ func TestInspectVMRunRepoRejectsSubmodules(t *testing.T) { func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) { repoRoot := t.TempDir() + repoCopyDir := filepath.Join(t.TempDir(), "repo-copy") origBegin := vmCreateBeginFunc origStatus := vmCreateStatusFunc origCancel := vmCreateCancelFunc origWaitForSSH := guestWaitForSSHFunc origGuestDial := guestDialFunc - origHostCommandOutput := hostCommandOutputFunc + origPrepareVMRunRepoCopy := prepareVMRunRepoCopyFunc origOpencodeExec := opencodeExecFunc t.Cleanup(func() { vmCreateBeginFunc = origBegin @@ -1032,7 +1057,7 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) { vmCreateCancelFunc = origCancel guestWaitForSSHFunc = origWaitForSSH guestDialFunc = origGuestDial - hostCommandOutputFunc = origHostCommandOutput + prepareVMRunRepoCopyFunc = origPrepareVMRunRepoCopy opencodeExecFunc = origOpencodeExec }) @@ -1082,20 +1107,11 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) { dialKeyPath = privateKeyPath return fakeClient, nil } - hostCommandOutputFunc = func(ctx context.Context, name string, args ...string) ([]byte, error) { - if name != "git" { - t.Fatalf("command = %q, want git", name) + prepareVMRunRepoCopyFunc = func(ctx context.Context, spec vmRunRepoSpec) (string, func(), error) { + if spec.RepoRoot != repoRoot { + t.Fatalf("spec.RepoRoot = %q, want %q", spec.RepoRoot, repoRoot) } - if len(args) < 7 || args[0] != "-C" || args[1] != repoRoot || args[2] != "bundle" || args[3] != "create" || args[5] != "--all" { - t.Fatalf("unexpected bundle args: %v", args) - } - if !reflect.DeepEqual(args[6:], []string{"deadbeef", "cafebabe"}) { - t.Fatalf("bundle revs = %v, want deadbeef/cafebabe", args[6:]) - } - if err := os.WriteFile(args[4], []byte("bundle-data"), 0o600); err != nil { - t.Fatalf("WriteFile(bundle): %v", err) - } - return nil, nil + return repoCopyDir, func() {}, nil } var attachArgs []string opencodeExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error { @@ -1143,21 +1159,18 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) { if dialKeyPath != waitKeyPath { t.Fatalf("dialKeyPath = %q, want %q", dialKeyPath, waitKeyPath) } - if fakeClient.uploadPath != vmRunGuestBundlePath { - t.Fatalf("uploadPath = %q, want %q", fakeClient.uploadPath, vmRunGuestBundlePath) + if fakeClient.tarSourceDir != repoCopyDir { + t.Fatalf("tarSourceDir = %q, want %q", fakeClient.tarSourceDir, repoCopyDir) } - if fakeClient.uploadMode != 0o600 { - t.Fatalf("uploadMode = %v, want 0600", fakeClient.uploadMode) - } - if string(fakeClient.uploadData) != "bundle-data" { - t.Fatalf("uploadData = %q, want bundle-data", string(fakeClient.uploadData)) - } - if !strings.Contains(fakeClient.script, `git clone "$BUNDLE" "$DIR"`) { - t.Fatalf("script = %q, want clone command", fakeClient.script) + if fakeClient.tarCommand != "rm -rf '/root/repo' && mkdir -p '/root/repo' && tar -o -C '/root/repo' --strip-components=1 -xf -" { + t.Fatalf("tarCommand = %q", fakeClient.tarCommand) } if !strings.Contains(fakeClient.script, `git -C "$DIR" checkout -B 'feature' 'cafebabe'`) { t.Fatalf("script = %q, want guest branch checkout", fakeClient.script) } + if !strings.Contains(fakeClient.script, `find "$DIR" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +`) { + t.Fatalf("script = %q, want guest worktree reset", fakeClient.script) + } if !strings.Contains(fakeClient.script, `git config --global --add safe.directory "$DIR"`) { t.Fatalf("script = %q, want guest safe.directory config", fakeClient.script) } @@ -1185,8 +1198,147 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) { } } -func TestVMRunCloneScriptSkipsRepoGitIdentityWhenIncomplete(t *testing.T) { - script := vmRunCloneScript(vmRunRepoSpec{ +func TestVMRunPrintsPostCreateProgress(t *testing.T) { + origBegin := vmCreateBeginFunc + origStatus := vmCreateStatusFunc + origCancel := vmCreateCancelFunc + origWaitForSSH := guestWaitForSSHFunc + origGuestDial := guestDialFunc + origPrepareVMRunRepoCopy := prepareVMRunRepoCopyFunc + origOpencodeExec := opencodeExecFunc + t.Cleanup(func() { + vmCreateBeginFunc = origBegin + vmCreateStatusFunc = origStatus + vmCreateCancelFunc = origCancel + guestWaitForSSHFunc = origWaitForSSH + guestDialFunc = origGuestDial + prepareVMRunRepoCopyFunc = origPrepareVMRunRepoCopy + opencodeExecFunc = origOpencodeExec + }) + + vm := model.VMRecord{ + ID: "vm-id", + Name: "devbox", + Runtime: model.VMRuntime{ + State: model.VMStateRunning, + GuestIP: "172.16.0.2", + }, + } + vmCreateBeginFunc = func(context.Context, string, api.VMCreateParams) (api.VMCreateBeginResult, error) { + return api.VMCreateBeginResult{ + Operation: api.VMCreateOperation{ + ID: "op-1", + Stage: "ready", + Detail: "vm is ready", + Done: true, + Success: true, + VM: &vm, + }, + }, nil + } + vmCreateStatusFunc = func(context.Context, string, string) (api.VMCreateStatusResult, error) { + t.Fatal("vmCreateStatusFunc should not be called") + return api.VMCreateStatusResult{}, nil + } + vmCreateCancelFunc = func(context.Context, string, string) error { + t.Fatal("vmCreateCancelFunc should not be called") + return nil + } + guestWaitForSSHFunc = func(ctx context.Context, address, privateKeyPath string, interval time.Duration) error { + return nil + } + guestDialFunc = func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error) { + return &testVMRunGuestClient{}, nil + } + prepareVMRunRepoCopyFunc = func(ctx context.Context, spec vmRunRepoSpec) (string, func(), error) { + return t.TempDir(), func() {}, nil + } + opencodeExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error { + return nil + } + + var stderr bytes.Buffer + err := runVMRun( + context.Background(), + "/tmp/bangerd.sock", + model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"}, + strings.NewReader(""), + &bytes.Buffer{}, + &stderr, + api.VMCreateParams{Name: "devbox"}, + vmRunRepoSpec{RepoRoot: t.TempDir(), RepoName: "repo", HeadCommit: "deadbeef"}, + ) + if err != nil { + t.Fatalf("runVMRun: %v", err) + } + + output := stderr.String() + for _, want := range []string{ + "[vm run] waiting for guest ssh", + "[vm run] preparing shallow repo", + "[vm run] copying repo metadata to guest", + "[vm run] preparing guest checkout", + "[vm run] overlaying host working tree", + "[vm run] attaching opencode", + } { + if !strings.Contains(output, want) { + t.Fatalf("stderr = %q, want %q", output, want) + } + } +} + +func TestPrepareVMRunRepoCopyCreatesShallowMetadataCopy(t *testing.T) { + if _, err := exec.LookPath("git"); err != nil { + t.Skip("git not installed") + } + + repoRoot := t.TempDir() + testRunGit(t, repoRoot, "init") + testRunGit(t, repoRoot, "remote", "add", "origin", "https://example.com/repo.git") + for i := 0; i < 12; i++ { + name := fmt.Sprintf("file-%02d.txt", i) + if err := os.WriteFile(filepath.Join(repoRoot, name), []byte(fmt.Sprintf("commit-%02d\n", i)), 0o644); err != nil { + t.Fatalf("WriteFile(%s): %v", name, err) + } + testRunGit(t, repoRoot, "add", name) + testRunGit(t, repoRoot, "commit", "-m", fmt.Sprintf("commit-%02d", i)) + } + baseCommit := strings.TrimSpace(testRunGit(t, repoRoot, "rev-parse", "HEAD~5")) + + repoCopyDir, cleanup, err := prepareVMRunRepoCopy(context.Background(), vmRunRepoSpec{ + RepoRoot: repoRoot, + RepoName: "repo", + BranchName: "feature", + BaseCommit: baseCommit, + HeadCommit: strings.TrimSpace(testRunGit(t, repoRoot, "rev-parse", "HEAD")), + OriginURL: "https://example.com/repo.git", + OverlayPaths: []string{"file-11.txt"}, + }) + if err != nil { + t.Fatalf("prepareVMRunRepoCopy: %v", err) + } + defer cleanup() + + entries, err := os.ReadDir(repoCopyDir) + if err != nil { + t.Fatalf("ReadDir(repoCopyDir): %v", err) + } + if len(entries) != 1 || entries[0].Name() != ".git" { + t.Fatalf("repo copy entries = %v, want only .git", entries) + } + if got := strings.TrimSpace(testRunGit(t, repoCopyDir, "rev-parse", "--is-shallow-repository")); got != "true" { + t.Fatalf("is-shallow-repository = %q, want true", got) + } + if got := strings.TrimSpace(testRunGit(t, repoCopyDir, "config", "--get", "remote.origin.url")); got != "https://example.com/repo.git" { + t.Fatalf("remote.origin.url = %q, want https://example.com/repo.git", got) + } + if _, err := exec.Command("git", "-C", repoCopyDir, "cat-file", "-e", baseCommit+"^{commit}").CombinedOutput(); err != nil { + t.Fatalf("cat-file -e %s^{commit}: %v", baseCommit, err) + } +} + +func TestVMRunCheckoutScriptSkipsRepoGitIdentityWhenIncomplete(t *testing.T) { + script := vmRunCheckoutScript(vmRunRepoSpec{ RepoName: "repo", HeadCommit: "deadbeef", CurrentBranch: "main", @@ -1388,10 +1540,9 @@ func testRunGit(t *testing.T, dir string, args ...string) string { type testVMRunGuestClient struct { closed bool - uploadPath string - uploadMode os.FileMode - uploadData []byte script string + tarSourceDir string + tarCommand string streamSourceDir string streamEntries []string streamCommand string @@ -1402,10 +1553,9 @@ func (c *testVMRunGuestClient) Close() error { return nil } -func (c *testVMRunGuestClient) UploadFile(ctx context.Context, remotePath string, mode os.FileMode, data []byte, logWriter io.Writer) error { - c.uploadPath = remotePath - c.uploadMode = mode - c.uploadData = append([]byte(nil), data...) +func (c *testVMRunGuestClient) StreamTar(ctx context.Context, sourceDir, remoteCommand string, logWriter io.Writer) error { + c.tarSourceDir = sourceDir + c.tarCommand = remoteCommand return nil }