Speed up vm run repo import
Replace the post-boot full-history git bundle path with a shallow repo copy so vm run no longer spends its quiet time shipping and cloning every object in the source repository. Stage a depth-10 no-checkout clone from the host repo, fetch the requested checkout commit only when it is outside the shallow window, rewrite origin back to the host repo's origin URL, and keep the existing guest checkout plus working-tree overlay behavior. Add explicit [vm run] progress lines after [vm create] ready so the user can see the SSH wait, shallow repo prep, guest copy, overlay, and opencode attach phases instead of a silent pause. Validated with GOCACHE=/tmp/banger-gocache go test ./..., make build, and a local payload comparison showing the banger repo dropping from a ~400 MB full bundle to a ~294 KB shallow metadata copy.
This commit is contained in:
parent
42b4a18c63
commit
1e967140c3
2 changed files with 299 additions and 83 deletions
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -93,13 +94,14 @@ var (
|
||||||
guestDialFunc = func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error) {
|
guestDialFunc = func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error) {
|
||||||
return guest.Dial(ctx, address, privateKeyPath)
|
return guest.Dial(ctx, address, privateKeyPath)
|
||||||
}
|
}
|
||||||
cwdFunc = os.Getwd
|
prepareVMRunRepoCopyFunc = prepareVMRunRepoCopy
|
||||||
|
cwdFunc = os.Getwd
|
||||||
)
|
)
|
||||||
|
|
||||||
type vmRunGuestClient interface {
|
type vmRunGuestClient interface {
|
||||||
Close() error
|
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
|
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
|
StreamTarEntries(ctx context.Context, sourceDir string, entries []string, remoteCommand string, logWriter io.Writer) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,12 +113,13 @@ type vmRunRepoSpec struct {
|
||||||
CurrentBranch string
|
CurrentBranch string
|
||||||
BranchName string
|
BranchName string
|
||||||
BaseCommit string
|
BaseCommit string
|
||||||
|
OriginURL string
|
||||||
GitUserName string
|
GitUserName string
|
||||||
GitUserEmail string
|
GitUserEmail string
|
||||||
OverlayPaths []string
|
OverlayPaths []string
|
||||||
}
|
}
|
||||||
|
|
||||||
const vmRunGuestBundlePath = "/tmp/banger-vm-run.bundle"
|
const vmRunShallowFetchDepth = 10
|
||||||
|
|
||||||
func NewBangerCommand() *cobra.Command {
|
func NewBangerCommand() *cobra.Command {
|
||||||
root := &cobra.Command{
|
root := &cobra.Command{
|
||||||
|
|
@ -1454,6 +1457,10 @@ func inspectVMRunRepo(ctx context.Context, rawPath, branchName, fromRef string)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vmRunRepoSpec{}, fmt.Errorf("resolve git user.email for %s: %w", repoRoot, err)
|
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)
|
overlayPaths, err := listVMRunOverlayPaths(ctx, repoRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1468,6 +1475,7 @@ func inspectVMRunRepo(ctx context.Context, rawPath, branchName, fromRef string)
|
||||||
CurrentBranch: currentBranch,
|
CurrentBranch: currentBranch,
|
||||||
BranchName: branchName,
|
BranchName: branchName,
|
||||||
BaseCommit: baseCommit,
|
BaseCommit: baseCommit,
|
||||||
|
OriginURL: originURL,
|
||||||
GitUserName: gitUserName,
|
GitUserName: gitUserName,
|
||||||
GitUserEmail: gitUserEmail,
|
GitUserEmail: gitUserEmail,
|
||||||
OverlayPaths: overlayPaths,
|
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 {
|
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)
|
vm, err := runVMCreate(ctx, socketPath, stderr, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -1592,6 +1601,7 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
|
||||||
vmRef = shortID(vm.ID)
|
vmRef = shortID(vm.ID)
|
||||||
}
|
}
|
||||||
sshAddress := net.JoinHostPort(vm.Runtime.GuestIP, "22")
|
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 {
|
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)
|
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)
|
return fmt.Errorf("vm %q is running but guest ssh is unavailable: %w", vmRef, err)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
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)
|
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 {
|
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 fmt.Errorf("vm %q is running but opencode attach failed: %w", vmRef, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func importVMRunRepoToGuest(ctx context.Context, client vmRunGuestClient, spec vmRunRepoSpec) error {
|
func importVMRunRepoToGuest(ctx context.Context, client vmRunGuestClient, spec vmRunRepoSpec, progress *vmRunProgressRenderer) error {
|
||||||
bundleData, err := createVMRunBundle(ctx, spec)
|
if progress != nil {
|
||||||
|
progress.render("preparing shallow repo")
|
||||||
|
}
|
||||||
|
repoCopyDir, cleanup, err := prepareVMRunRepoCopyFunc(ctx, spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var uploadLog bytes.Buffer
|
defer cleanup()
|
||||||
if err := client.UploadFile(ctx, vmRunGuestBundlePath, 0o600, bundleData, &uploadLog); err != nil {
|
if progress != nil {
|
||||||
return formatVMRunStepError("upload git bundle", err, uploadLog.String())
|
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
|
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())
|
return formatVMRunStepError("prepare guest checkout", err, scriptLog.String())
|
||||||
}
|
}
|
||||||
|
if progress != nil {
|
||||||
|
progress.render("overlaying host working tree")
|
||||||
|
}
|
||||||
var overlayLog bytes.Buffer
|
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 {
|
if err := client.StreamTarEntries(ctx, spec.RepoRoot, spec.OverlayPaths, remoteCommand, &overlayLog); err != nil {
|
||||||
return formatVMRunStepError("overlay host working tree", err, overlayLog.String())
|
return formatVMRunStepError("overlay host working tree", err, overlayLog.String())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createVMRunBundle(ctx context.Context, spec vmRunRepoSpec) ([]byte, error) {
|
func prepareVMRunRepoCopy(ctx context.Context, spec vmRunRepoSpec) (string, func(), error) {
|
||||||
tempFile, err := os.CreateTemp("", "banger-vm-run-*.bundle")
|
tempRoot, err := os.MkdirTemp("", "banger-vm-run-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
tempPath := tempFile.Name()
|
cleanup := func() {
|
||||||
if err := tempFile.Close(); err != nil {
|
_ = os.RemoveAll(tempRoot)
|
||||||
_ = os.Remove(tempPath)
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
defer os.Remove(tempPath)
|
repoCopyDir := filepath.Join(tempRoot, spec.RepoName)
|
||||||
|
cloneArgs := []string{"clone", "--no-checkout", "--depth", fmt.Sprintf("%d", vmRunShallowFetchDepth)}
|
||||||
args := []string{"-C", spec.RepoRoot, "bundle", "create", tempPath, "--all"}
|
if strings.TrimSpace(spec.CurrentBranch) != "" {
|
||||||
for _, rev := range uniqueNonEmptyStrings(spec.HeadCommit, spec.BaseCommit) {
|
cloneArgs = append(cloneArgs, "--single-branch", "--branch", spec.CurrentBranch)
|
||||||
args = append(args, rev)
|
|
||||||
}
|
}
|
||||||
if _, err := hostCommandOutputFunc(ctx, "git", args...); err != nil {
|
cloneArgs = append(cloneArgs, gitFileURL(spec.RepoRoot), repoCopyDir)
|
||||||
return nil, fmt.Errorf("create git bundle: %w", err)
|
if err := runHostCommand(ctx, "git", cloneArgs...); err != nil {
|
||||||
|
cleanup()
|
||||||
|
return "", nil, fmt.Errorf("clone shallow repo copy: %w", err)
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(tempPath)
|
checkoutCommit := vmRunCheckoutCommit(spec)
|
||||||
if err != nil {
|
if err := runHostCommand(ctx, "git", "-C", repoCopyDir, "cat-file", "-e", checkoutCommit+"^{commit}"); err != nil {
|
||||||
return nil, fmt.Errorf("read git bundle: %w", err)
|
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)
|
guestDir := vmRunGuestDir(spec.RepoName)
|
||||||
var script strings.Builder
|
var script strings.Builder
|
||||||
script.WriteString("set -euo pipefail\n")
|
script.WriteString("set -euo pipefail\n")
|
||||||
fmt.Fprintf(&script, "DIR=%s\n", shellQuote(guestDir))
|
fmt.Fprintf(&script, "DIR=%s\n", shellQuote(guestDir))
|
||||||
fmt.Fprintf(&script, "BUNDLE=%s\n", shellQuote(vmRunGuestBundlePath))
|
script.WriteString("git config --global --add safe.directory \"$DIR\"\n")
|
||||||
script.WriteString("rm -rf \"$DIR\"\n")
|
|
||||||
script.WriteString("git clone \"$BUNDLE\" \"$DIR\"\n")
|
|
||||||
script.WriteString("rm -f \"$BUNDLE\"\n")
|
|
||||||
switch {
|
switch {
|
||||||
case strings.TrimSpace(spec.BranchName) != "":
|
case strings.TrimSpace(spec.BranchName) != "":
|
||||||
fmt.Fprintf(&script, "git -C \"$DIR\" checkout -B %s %s\n", shellQuote(spec.BranchName), shellQuote(spec.BaseCommit))
|
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))
|
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("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) != "" {
|
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.name %s\n", shellQuote(spec.GitUserName))
|
||||||
fmt.Fprintf(&script, "git -C \"$DIR\" config user.email %s\n", shellQuote(spec.GitUserEmail))
|
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)
|
return fmt.Errorf("%s: %w: %s", action, err, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
func uniqueNonEmptyStrings(values ...string) []string {
|
type vmRunProgressRenderer struct {
|
||||||
unique := make([]string, 0, len(values))
|
out io.Writer
|
||||||
seen := make(map[string]struct{}, len(values))
|
enabled bool
|
||||||
for _, value := range values {
|
lastLine string
|
||||||
value = strings.TrimSpace(value)
|
}
|
||||||
if value == "" {
|
|
||||||
continue
|
func newVMRunProgressRenderer(out io.Writer) *vmRunProgressRenderer {
|
||||||
}
|
return &vmRunProgressRenderer{
|
||||||
if _, ok := seen[value]; ok {
|
out: out,
|
||||||
continue
|
enabled: out != nil,
|
||||||
}
|
|
||||||
seen[value] = struct{}{}
|
|
||||||
unique = append(unique, value)
|
|
||||||
}
|
}
|
||||||
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 {
|
func shellQuote(value string) string {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestVMSetParamsFromFlagsConflict(t *testing.T) {
|
||||||
if _, err := vmSetParamsFromFlags("devbox", -1, -1, "", true, true); err == nil {
|
if _, err := vmSetParamsFromFlags("devbox", -1, -1, "", true, true); err == nil {
|
||||||
t.Fatal("expected nat conflict error")
|
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.email", "global@example.com")
|
||||||
testRunGit(t, repoRoot, "config", "--global", "user.name", "Global User")
|
testRunGit(t, repoRoot, "config", "--global", "user.name", "Global User")
|
||||||
testRunGit(t, repoRoot, "init")
|
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.email", "test@example.com")
|
||||||
testRunGit(t, repoRoot, "config", "user.name", "Banger Test")
|
testRunGit(t, repoRoot, "config", "user.name", "Banger Test")
|
||||||
|
|
||||||
|
|
@ -972,6 +993,9 @@ func TestInspectVMRunRepoUsesRepoRootAndOverlayPaths(t *testing.T) {
|
||||||
if spec.BaseCommit != spec.HeadCommit {
|
if spec.BaseCommit != spec.HeadCommit {
|
||||||
t.Fatalf("BaseCommit = %q, want head %q", 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" {
|
if spec.GitUserName != "Banger Test" {
|
||||||
t.Fatalf("GitUserName = %q, want Banger Test", spec.GitUserName)
|
t.Fatalf("GitUserName = %q, want Banger Test", spec.GitUserName)
|
||||||
}
|
}
|
||||||
|
|
@ -1018,13 +1042,14 @@ func TestInspectVMRunRepoRejectsSubmodules(t *testing.T) {
|
||||||
|
|
||||||
func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
|
func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
|
||||||
repoRoot := t.TempDir()
|
repoRoot := t.TempDir()
|
||||||
|
repoCopyDir := filepath.Join(t.TempDir(), "repo-copy")
|
||||||
|
|
||||||
origBegin := vmCreateBeginFunc
|
origBegin := vmCreateBeginFunc
|
||||||
origStatus := vmCreateStatusFunc
|
origStatus := vmCreateStatusFunc
|
||||||
origCancel := vmCreateCancelFunc
|
origCancel := vmCreateCancelFunc
|
||||||
origWaitForSSH := guestWaitForSSHFunc
|
origWaitForSSH := guestWaitForSSHFunc
|
||||||
origGuestDial := guestDialFunc
|
origGuestDial := guestDialFunc
|
||||||
origHostCommandOutput := hostCommandOutputFunc
|
origPrepareVMRunRepoCopy := prepareVMRunRepoCopyFunc
|
||||||
origOpencodeExec := opencodeExecFunc
|
origOpencodeExec := opencodeExecFunc
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
vmCreateBeginFunc = origBegin
|
vmCreateBeginFunc = origBegin
|
||||||
|
|
@ -1032,7 +1057,7 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
|
||||||
vmCreateCancelFunc = origCancel
|
vmCreateCancelFunc = origCancel
|
||||||
guestWaitForSSHFunc = origWaitForSSH
|
guestWaitForSSHFunc = origWaitForSSH
|
||||||
guestDialFunc = origGuestDial
|
guestDialFunc = origGuestDial
|
||||||
hostCommandOutputFunc = origHostCommandOutput
|
prepareVMRunRepoCopyFunc = origPrepareVMRunRepoCopy
|
||||||
opencodeExecFunc = origOpencodeExec
|
opencodeExecFunc = origOpencodeExec
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1082,20 +1107,11 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
|
||||||
dialKeyPath = privateKeyPath
|
dialKeyPath = privateKeyPath
|
||||||
return fakeClient, nil
|
return fakeClient, nil
|
||||||
}
|
}
|
||||||
hostCommandOutputFunc = func(ctx context.Context, name string, args ...string) ([]byte, error) {
|
prepareVMRunRepoCopyFunc = func(ctx context.Context, spec vmRunRepoSpec) (string, func(), error) {
|
||||||
if name != "git" {
|
if spec.RepoRoot != repoRoot {
|
||||||
t.Fatalf("command = %q, want git", name)
|
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" {
|
return repoCopyDir, func() {}, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
var attachArgs []string
|
var attachArgs []string
|
||||||
opencodeExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
|
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 {
|
if dialKeyPath != waitKeyPath {
|
||||||
t.Fatalf("dialKeyPath = %q, want %q", dialKeyPath, waitKeyPath)
|
t.Fatalf("dialKeyPath = %q, want %q", dialKeyPath, waitKeyPath)
|
||||||
}
|
}
|
||||||
if fakeClient.uploadPath != vmRunGuestBundlePath {
|
if fakeClient.tarSourceDir != repoCopyDir {
|
||||||
t.Fatalf("uploadPath = %q, want %q", fakeClient.uploadPath, vmRunGuestBundlePath)
|
t.Fatalf("tarSourceDir = %q, want %q", fakeClient.tarSourceDir, repoCopyDir)
|
||||||
}
|
}
|
||||||
if fakeClient.uploadMode != 0o600 {
|
if fakeClient.tarCommand != "rm -rf '/root/repo' && mkdir -p '/root/repo' && tar -o -C '/root/repo' --strip-components=1 -xf -" {
|
||||||
t.Fatalf("uploadMode = %v, want 0600", fakeClient.uploadMode)
|
t.Fatalf("tarCommand = %q", fakeClient.tarCommand)
|
||||||
}
|
|
||||||
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 !strings.Contains(fakeClient.script, `git -C "$DIR" checkout -B 'feature' 'cafebabe'`) {
|
if !strings.Contains(fakeClient.script, `git -C "$DIR" checkout -B 'feature' 'cafebabe'`) {
|
||||||
t.Fatalf("script = %q, want guest branch checkout", fakeClient.script)
|
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"`) {
|
if !strings.Contains(fakeClient.script, `git config --global --add safe.directory "$DIR"`) {
|
||||||
t.Fatalf("script = %q, want guest safe.directory config", fakeClient.script)
|
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) {
|
func TestVMRunPrintsPostCreateProgress(t *testing.T) {
|
||||||
script := vmRunCloneScript(vmRunRepoSpec{
|
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",
|
RepoName: "repo",
|
||||||
HeadCommit: "deadbeef",
|
HeadCommit: "deadbeef",
|
||||||
CurrentBranch: "main",
|
CurrentBranch: "main",
|
||||||
|
|
@ -1388,10 +1540,9 @@ func testRunGit(t *testing.T, dir string, args ...string) string {
|
||||||
|
|
||||||
type testVMRunGuestClient struct {
|
type testVMRunGuestClient struct {
|
||||||
closed bool
|
closed bool
|
||||||
uploadPath string
|
|
||||||
uploadMode os.FileMode
|
|
||||||
uploadData []byte
|
|
||||||
script string
|
script string
|
||||||
|
tarSourceDir string
|
||||||
|
tarCommand string
|
||||||
streamSourceDir string
|
streamSourceDir string
|
||||||
streamEntries []string
|
streamEntries []string
|
||||||
streamCommand string
|
streamCommand string
|
||||||
|
|
@ -1402,10 +1553,9 @@ func (c *testVMRunGuestClient) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testVMRunGuestClient) UploadFile(ctx context.Context, remotePath string, mode os.FileMode, data []byte, logWriter io.Writer) error {
|
func (c *testVMRunGuestClient) StreamTar(ctx context.Context, sourceDir, remoteCommand string, logWriter io.Writer) error {
|
||||||
c.uploadPath = remotePath
|
c.tarSourceDir = sourceDir
|
||||||
c.uploadMode = mode
|
c.tarCommand = remoteCommand
|
||||||
c.uploadData = append([]byte(nil), data...)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue