cli: delete vm run's dead import path + duplicated git inspection
The CLI carried a full second copy of the workspace import
implementation that `vm run` never actually used:
- importVMRunRepoToGuest (no callers — the live flow calls the
daemon's PrepareVMWorkspace RPC instead)
- prepareVMRunRepoCopy, vmRunCheckoutCommit, vmRunCheckoutScript,
gitFileURL, runHostCommand (all reachable only from the dead
importVMRunRepoToGuest)
Plus a duplicated repo-inspection surface that shadowed the
daemon's:
- inspectVMRunRepo ran every git query the daemon re-ran during
workspace.prepare (HEAD, branch, identity, origin, overlay list)
- gitOutput / gitTrimmedOutput / gitResolvedConfigValue /
parseNullSeparatedOutput / listSubmodules / listOverlayPaths /
resolveVMRunSourcePath — all identical to the exported
workspace.* versions
- vmRunRepoSpec — same fields as workspace.RepoSpec
Replaced with a single minimal preflight:
func vmRunPreflightRepo(ctx, rawPath) (absPath, err error)
The preflight only checks what the user can fix locally before
banger creates a VM (path exists, sits in a non-bare git repo, no
submodules). The daemon's workspace.prepare RPC does the full
inspection — and returns RepoRoot + RepoName in the response, which
the CLI now threads into the tooling harness instead of computing
them a second time.
Signature changes:
runVMRun(ctx, ..., *vmRunRepo, ...) // was: *vmRunRepoSpec
startVMRunToolingHarness(ctx, client, repoRoot, repoName, progress)
// was: (ctx, client, spec, progress)
vmRunToolingHarnessScript(plan) // was: (spec, plan)
vmRunToolingHarnessLaunchScript(repoName) // was: (spec)
Tests: the CLI-side git-inspection tests are replaced by a single
TestVMRunPreflightRejectsSubmodules that exercises the preflight.
Everything else (tooling harness script, progress renderer, SSH args,
runVMRun flows) keeps working. The shallow-copy / checkout-script
tests are gone — that code now lives only in
internal/daemon/workspace and is tested there.
Also fixed a latent bug the refactor exposed: vm run's --from flag
defaults to "HEAD", which the daemon reads as "from without branch"
and rejects. CLI now scrubs fromRef when branchName is empty.
Live verified: `banger vm run --name X . -- cmd` boots, workspace
materialises at /root/repo with matching HEAD, exit code propagates.
This commit is contained in:
parent
ae14b9499d
commit
3a5f4cd40d
2 changed files with 112 additions and 498 deletions
|
|
@ -13,7 +13,6 @@ import (
|
|||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -28,6 +27,7 @@ import (
|
|||
"banger/internal/buildinfo"
|
||||
"banger/internal/config"
|
||||
"banger/internal/daemon"
|
||||
"banger/internal/daemon/workspace"
|
||||
"banger/internal/guest"
|
||||
"banger/internal/hostnat"
|
||||
"banger/internal/imagecat"
|
||||
|
|
@ -138,7 +138,6 @@ var (
|
|||
knownHosts, _ := bangerKnownHostsPath()
|
||||
return guest.Dial(ctx, address, privateKeyPath, knownHosts)
|
||||
}
|
||||
prepareVMRunRepoCopyFunc = prepareVMRunRepoCopy
|
||||
buildVMRunToolingPlanFunc = toolingplan.Build
|
||||
cwdFunc = os.Getwd
|
||||
)
|
||||
|
|
@ -151,23 +150,17 @@ type vmRunGuestClient interface {
|
|||
StreamTarEntries(ctx context.Context, sourceDir string, entries []string, remoteCommand string, logWriter io.Writer) error
|
||||
}
|
||||
|
||||
type vmRunRepoSpec struct {
|
||||
SourcePath string
|
||||
RepoRoot string
|
||||
RepoName string
|
||||
HeadCommit string
|
||||
CurrentBranch string
|
||||
BranchName string
|
||||
FromRef string
|
||||
BaseCommit string
|
||||
OriginURL string
|
||||
GitUserName string
|
||||
GitUserEmail string
|
||||
OverlayPaths []string
|
||||
// vmRunRepo is the CLI-local view of the workspace argument to
|
||||
// `vm run`: an absolute source path that passed preflight, plus the
|
||||
// two branch flags. Everything else the flow needs (RepoRoot,
|
||||
// RepoName, HEAD commit, etc.) comes back from the workspace.prepare
|
||||
// RPC, which does the full git inspection daemon-side.
|
||||
type vmRunRepo struct {
|
||||
sourcePath string
|
||||
branchName string
|
||||
fromRef string
|
||||
}
|
||||
|
||||
const vmRunShallowFetchDepth = 10
|
||||
|
||||
const vmRunToolingInstallTimeoutSeconds = 120
|
||||
|
||||
// vmRunSSHTimeout bounds how long `vm run` waits for guest ssh after
|
||||
|
|
@ -791,13 +784,13 @@ Three modes:
|
|||
return errors.New("--branch requires a path argument")
|
||||
}
|
||||
|
||||
var specPtr *vmRunRepoSpec
|
||||
var repoPtr *vmRunRepo
|
||||
if sourcePath != "" {
|
||||
spec, err := inspectVMRunRepo(cmd.Context(), sourcePath, branchName, fromRef)
|
||||
resolved, err := vmRunPreflightRepo(cmd.Context(), sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
specPtr = &spec
|
||||
repoPtr = &vmRunRepo{sourcePath: resolved, branchName: branchName, fromRef: fromRef}
|
||||
}
|
||||
|
||||
layout, err := paths.Resolve()
|
||||
|
|
@ -808,7 +801,7 @@ Three modes:
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if specPtr != nil {
|
||||
if repoPtr != nil {
|
||||
if err := validateVMRunPrereqs(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -828,7 +821,7 @@ Three modes:
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runVMRun(cmd.Context(), layout.SocketPath, cfg, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), params, specPtr, commandArgs, removeOnExit)
|
||||
return runVMRun(cmd.Context(), layout.SocketPath, cfg, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), params, repoPtr, commandArgs, removeOnExit)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&name, "name", "", "vm name")
|
||||
|
|
@ -1280,7 +1273,14 @@ func newVMWorkspacePrepareCommand() *cobra.Command {
|
|||
if len(args) > 1 {
|
||||
sourcePath = args[1]
|
||||
}
|
||||
resolvedPath, err := resolveVMRunSourcePath(sourcePath)
|
||||
if strings.TrimSpace(sourcePath) == "" {
|
||||
wd, err := cwdFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourcePath = wd
|
||||
}
|
||||
resolvedPath, err := workspace.ResolveSourcePath(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -2732,86 +2732,20 @@ func validateVMRunPrereqs(cfg model.DaemonConfig) error {
|
|||
return checks.Err("vm run preflight failed")
|
||||
}
|
||||
|
||||
func inspectVMRunRepo(ctx context.Context, rawPath, branchName, fromRef string) (vmRunRepoSpec, error) {
|
||||
sourcePath, err := resolveVMRunSourcePath(rawPath)
|
||||
if err != nil {
|
||||
return vmRunRepoSpec{}, err
|
||||
}
|
||||
|
||||
repoRoot, err := gitTrimmedOutput(ctx, sourcePath, "rev-parse", "--show-toplevel")
|
||||
if err != nil {
|
||||
return vmRunRepoSpec{}, fmt.Errorf("%s is not inside a git repository", sourcePath)
|
||||
}
|
||||
isBare, err := gitTrimmedOutput(ctx, repoRoot, "rev-parse", "--is-bare-repository")
|
||||
if err != nil {
|
||||
return vmRunRepoSpec{}, fmt.Errorf("inspect git repository %s: %w", repoRoot, err)
|
||||
}
|
||||
if isBare == "true" {
|
||||
return vmRunRepoSpec{}, fmt.Errorf("vm run requires a non-bare git repository: %s", repoRoot)
|
||||
}
|
||||
if err := ensureVMRunRepoHasNoSubmodules(ctx, repoRoot); err != nil {
|
||||
return vmRunRepoSpec{}, err
|
||||
}
|
||||
|
||||
headCommit, err := gitTrimmedOutput(ctx, repoRoot, "rev-parse", "HEAD^{commit}")
|
||||
if err != nil {
|
||||
return vmRunRepoSpec{}, fmt.Errorf("git repository %s must have at least one commit", repoRoot)
|
||||
}
|
||||
currentBranch, err := gitTrimmedOutput(ctx, repoRoot, "branch", "--show-current")
|
||||
if err != nil {
|
||||
return vmRunRepoSpec{}, fmt.Errorf("resolve current branch for %s: %w", repoRoot, err)
|
||||
}
|
||||
|
||||
baseCommit := headCommit
|
||||
resolvedFromRef := ""
|
||||
branchName = strings.TrimSpace(branchName)
|
||||
if branchName != "" {
|
||||
fromRef = strings.TrimSpace(fromRef)
|
||||
if fromRef == "" {
|
||||
return vmRunRepoSpec{}, errors.New("--from cannot be empty")
|
||||
}
|
||||
resolvedFromRef = fromRef
|
||||
baseCommit, err = gitTrimmedOutput(ctx, repoRoot, "rev-parse", fromRef+"^{commit}")
|
||||
if err != nil {
|
||||
return vmRunRepoSpec{}, fmt.Errorf("resolve --from %q: %w", fromRef, err)
|
||||
}
|
||||
}
|
||||
|
||||
gitUserName, err := gitResolvedConfigValue(ctx, repoRoot, "user.name")
|
||||
if err != nil {
|
||||
return vmRunRepoSpec{}, fmt.Errorf("resolve git user.name for %s: %w", repoRoot, err)
|
||||
}
|
||||
gitUserEmail, err := gitResolvedConfigValue(ctx, repoRoot, "user.email")
|
||||
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 {
|
||||
return vmRunRepoSpec{}, err
|
||||
}
|
||||
|
||||
return vmRunRepoSpec{
|
||||
SourcePath: sourcePath,
|
||||
RepoRoot: repoRoot,
|
||||
RepoName: filepath.Base(repoRoot),
|
||||
HeadCommit: headCommit,
|
||||
CurrentBranch: currentBranch,
|
||||
BranchName: branchName,
|
||||
FromRef: resolvedFromRef,
|
||||
BaseCommit: baseCommit,
|
||||
OriginURL: originURL,
|
||||
GitUserName: gitUserName,
|
||||
GitUserEmail: gitUserEmail,
|
||||
OverlayPaths: overlayPaths,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveVMRunSourcePath(rawPath string) (string, error) {
|
||||
// vmRunPreflightRepo validates a vm run workspace path BEFORE the VM
|
||||
// is created, so bad paths fail fast instead of leaving the user
|
||||
// with an orphaned VM. The check is intentionally minimal: the
|
||||
// daemon's PrepareVMWorkspace does a full git inspection (branch,
|
||||
// HEAD, identity, overlay) and returns everything the tooling
|
||||
// harness needs, so duplicating the heavy lifting here just doubles
|
||||
// the I/O. We only enforce what the user can fix locally before
|
||||
// banger commits to creating a VM:
|
||||
//
|
||||
// - the path exists and is a directory,
|
||||
// - it sits inside a non-bare git repository,
|
||||
// - the repository has no submodules (unsupported in the shallow
|
||||
// overlay mode vm run uses).
|
||||
func vmRunPreflightRepo(ctx context.Context, rawPath string) (string, error) {
|
||||
if strings.TrimSpace(rawPath) == "" {
|
||||
wd, err := cwdFunc()
|
||||
if err != nil {
|
||||
|
|
@ -2819,104 +2753,29 @@ func resolveVMRunSourcePath(rawPath string) (string, error) {
|
|||
}
|
||||
rawPath = wd
|
||||
}
|
||||
absPath, err := filepath.Abs(rawPath)
|
||||
sourcePath, err := workspace.ResolveSourcePath(rawPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
info, err := os.Stat(absPath)
|
||||
repoRoot, err := workspace.GitTrimmedOutput(ctx, sourcePath, "rev-parse", "--show-toplevel")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s is not inside a git repository", sourcePath)
|
||||
}
|
||||
isBare, err := workspace.GitTrimmedOutput(ctx, repoRoot, "rev-parse", "--is-bare-repository")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("inspect git repository %s: %w", repoRoot, err)
|
||||
}
|
||||
if isBare == "true" {
|
||||
return "", fmt.Errorf("vm run requires a non-bare git repository: %s", repoRoot)
|
||||
}
|
||||
submodules, err := workspace.ListSubmodules(ctx, repoRoot)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", fmt.Errorf("%s is not a directory", absPath)
|
||||
if len(submodules) > 0 {
|
||||
return "", fmt.Errorf("vm run does not support git submodules in %s (%s); use `vm create` + `vm workspace prepare --mode full_copy`", repoRoot, strings.Join(submodules, ", "))
|
||||
}
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
func ensureVMRunRepoHasNoSubmodules(ctx context.Context, repoRoot string) error {
|
||||
output, err := gitOutput(ctx, repoRoot, "ls-files", "--stage", "-z")
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect git index for %s: %w", repoRoot, err)
|
||||
}
|
||||
for _, record := range parseNullSeparatedOutput(output) {
|
||||
if strings.HasPrefix(record, "160000 ") {
|
||||
return fmt.Errorf("vm run does not yet support git submodules: %s", repoRoot)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func listVMRunOverlayPaths(ctx context.Context, repoRoot string) ([]string, error) {
|
||||
trackedOutput, err := gitOutput(ctx, repoRoot, "ls-files", "-z")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list tracked files for %s: %w", repoRoot, err)
|
||||
}
|
||||
untrackedOutput, err := gitOutput(ctx, repoRoot, "ls-files", "--others", "--exclude-standard", "-z")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list untracked files for %s: %w", repoRoot, err)
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, relPath := range parseNullSeparatedOutput(trackedOutput) {
|
||||
if relPath == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(repoRoot, relPath)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
seen[relPath] = struct{}{}
|
||||
paths = append(paths, relPath)
|
||||
}
|
||||
for _, relPath := range parseNullSeparatedOutput(untrackedOutput) {
|
||||
if relPath == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[relPath]; ok {
|
||||
continue
|
||||
}
|
||||
seen[relPath] = struct{}{}
|
||||
paths = append(paths, relPath)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func gitOutput(ctx context.Context, dir string, args ...string) ([]byte, error) {
|
||||
fullArgs := make([]string, 0, len(args)+2)
|
||||
if strings.TrimSpace(dir) != "" {
|
||||
fullArgs = append(fullArgs, "-C", dir)
|
||||
}
|
||||
fullArgs = append(fullArgs, args...)
|
||||
return hostCommandOutputFunc(ctx, "git", fullArgs...)
|
||||
}
|
||||
|
||||
func gitTrimmedOutput(ctx context.Context, dir string, args ...string) (string, error) {
|
||||
output, err := gitOutput(ctx, dir, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
func gitResolvedConfigValue(ctx context.Context, dir, key string) (string, error) {
|
||||
return gitTrimmedOutput(ctx, dir, "config", "--default", "", "--get", key)
|
||||
}
|
||||
|
||||
func parseNullSeparatedOutput(output []byte) []string {
|
||||
chunks := bytes.Split(output, []byte{0})
|
||||
values := make([]string, 0, len(chunks))
|
||||
for _, chunk := range chunks {
|
||||
value := strings.TrimSpace(string(chunk))
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values
|
||||
return sourcePath, nil
|
||||
}
|
||||
|
||||
// splitVMRunArgs partitions cobra positional args into the optional path
|
||||
|
|
@ -2946,7 +2805,7 @@ func (e ExitCodeError) Error() string {
|
|||
return fmt.Sprintf("exit status %d", e.Code)
|
||||
}
|
||||
|
||||
func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, stdin io.Reader, stdout, stderr io.Writer, params api.VMCreateParams, spec *vmRunRepoSpec, command []string, removeOnExit bool) error {
|
||||
func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, stdin io.Reader, stdout, stderr io.Writer, params api.VMCreateParams, repo *vmRunRepo, command []string, removeOnExit bool) error {
|
||||
progress := newVMRunProgressRenderer(stderr)
|
||||
vm, err := runVMCreate(ctx, socketPath, stderr, params)
|
||||
if err != nil {
|
||||
|
|
@ -2997,24 +2856,37 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
|
|||
}
|
||||
cancelSSH()
|
||||
shouldRemove = removeOnExit
|
||||
if spec != nil {
|
||||
if repo != nil {
|
||||
progress.render("preparing guest workspace")
|
||||
if _, err := vmWorkspacePrepareFunc(ctx, socketPath, api.VMWorkspacePrepareParams{
|
||||
// --from is only meaningful paired with --branch; the daemon
|
||||
// rejects "from without branch" outright. Our flag default is
|
||||
// "HEAD" (useful only when --branch is set), so scrub it when
|
||||
// branch is empty to avoid a false "workspace from requires
|
||||
// branch" error.
|
||||
fromRef := ""
|
||||
if strings.TrimSpace(repo.branchName) != "" {
|
||||
fromRef = repo.fromRef
|
||||
}
|
||||
prepared, err := vmWorkspacePrepareFunc(ctx, socketPath, api.VMWorkspacePrepareParams{
|
||||
IDOrName: vmRef,
|
||||
SourcePath: spec.SourcePath,
|
||||
SourcePath: repo.sourcePath,
|
||||
GuestPath: vmRunGuestDir(),
|
||||
Branch: spec.BranchName,
|
||||
From: spec.FromRef,
|
||||
Branch: repo.branchName,
|
||||
From: fromRef,
|
||||
Mode: string(model.WorkspacePrepareModeShallowOverlay),
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("vm %q is running but workspace prepare failed: %w", vmRef, err)
|
||||
}
|
||||
// The prepare RPC already did the full git inspection on the
|
||||
// daemon side; grab what the tooling harness needs from its
|
||||
// result instead of re-inspecting here.
|
||||
if len(command) == 0 {
|
||||
client, err := guestDialFunc(ctx, sshAddress, cfg.SSHKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("vm %q is running but guest ssh is unavailable: %w", vmRef, err)
|
||||
}
|
||||
if err := startVMRunToolingHarness(ctx, client, *spec, progress); err != nil {
|
||||
if err := startVMRunToolingHarness(ctx, client, prepared.Workspace.RepoRoot, prepared.Workspace.RepoName, progress); err != nil {
|
||||
printVMRunWarning(stderr, fmt.Sprintf("guest tooling bootstrap start failed: %v", err))
|
||||
}
|
||||
_ = client.Close()
|
||||
|
|
@ -3039,118 +2911,6 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
|
|||
return runSSHSession(ctx, socketPath, vmRef, stdin, stdout, stderr, sshArgs, removeOnExit)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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()), shellQuote(vmRunGuestDir()), shellQuote(vmRunGuestDir()))
|
||||
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, 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()))
|
||||
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 prepareVMRunRepoCopy(ctx context.Context, spec vmRunRepoSpec) (string, func(), error) {
|
||||
tempRoot, err := os.MkdirTemp("", "banger-vm-run-*")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
cleanup := func() {
|
||||
_ = os.RemoveAll(tempRoot)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 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()
|
||||
var script strings.Builder
|
||||
script.WriteString("set -euo pipefail\n")
|
||||
fmt.Fprintf(&script, "DIR=%s\n", shellQuote(guestDir))
|
||||
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))
|
||||
case strings.TrimSpace(spec.CurrentBranch) != "":
|
||||
fmt.Fprintf(&script, "git -C \"$DIR\" checkout -B %s %s\n", shellQuote(spec.CurrentBranch), shellQuote(spec.HeadCommit))
|
||||
default:
|
||||
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")
|
||||
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))
|
||||
}
|
||||
return script.String()
|
||||
}
|
||||
|
||||
func vmRunGuestDir() string {
|
||||
return "/root/repo"
|
||||
}
|
||||
|
|
@ -3164,26 +2924,30 @@ func vmRunToolingHarnessLogPath(repoName string) string {
|
|||
return filepath.ToSlash(filepath.Join("/root/.cache/banger", "vm-run-tooling-"+repoName+".log"))
|
||||
}
|
||||
|
||||
func startVMRunToolingHarness(ctx context.Context, client vmRunGuestClient, spec vmRunRepoSpec, progress *vmRunProgressRenderer) error {
|
||||
// startVMRunToolingHarness uploads + launches the mise bootstrap
|
||||
// script inside the guest. repoRoot / repoName both come from the
|
||||
// daemon's workspace.prepare RPC response — the CLI no longer does
|
||||
// its own git inspection.
|
||||
func startVMRunToolingHarness(ctx context.Context, client vmRunGuestClient, repoRoot, repoName string, progress *vmRunProgressRenderer) error {
|
||||
if progress != nil {
|
||||
progress.render("starting guest tooling bootstrap")
|
||||
}
|
||||
plan := buildVMRunToolingPlanFunc(ctx, spec.RepoRoot)
|
||||
plan := buildVMRunToolingPlanFunc(ctx, repoRoot)
|
||||
var uploadLog bytes.Buffer
|
||||
if err := client.UploadFile(ctx, vmRunToolingHarnessPath(spec.RepoName), 0o755, []byte(vmRunToolingHarnessScript(spec, plan)), &uploadLog); err != nil {
|
||||
if err := client.UploadFile(ctx, vmRunToolingHarnessPath(repoName), 0o755, []byte(vmRunToolingHarnessScript(plan)), &uploadLog); err != nil {
|
||||
return formatVMRunStepError("upload guest tooling bootstrap", err, uploadLog.String())
|
||||
}
|
||||
var launchLog bytes.Buffer
|
||||
if err := client.RunScript(ctx, vmRunToolingHarnessLaunchScript(spec), &launchLog); err != nil {
|
||||
if err := client.RunScript(ctx, vmRunToolingHarnessLaunchScript(repoName), &launchLog); err != nil {
|
||||
return formatVMRunStepError("launch guest tooling bootstrap", err, launchLog.String())
|
||||
}
|
||||
if progress != nil {
|
||||
progress.render("guest tooling log: " + vmRunToolingHarnessLogPath(spec.RepoName))
|
||||
progress.render("guest tooling log: " + vmRunToolingHarnessLogPath(repoName))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func vmRunToolingHarnessScript(spec vmRunRepoSpec, plan toolingplan.Plan) string {
|
||||
func vmRunToolingHarnessScript(plan toolingplan.Plan) string {
|
||||
var script strings.Builder
|
||||
script.WriteString("set -uo pipefail\n")
|
||||
fmt.Fprintf(&script, "DIR=%s\n", shellQuote(vmRunGuestDir()))
|
||||
|
|
@ -3260,11 +3024,11 @@ func vmRunToolingHarnessScript(spec vmRunRepoSpec, plan toolingplan.Plan) string
|
|||
return script.String()
|
||||
}
|
||||
|
||||
func vmRunToolingHarnessLaunchScript(spec vmRunRepoSpec) string {
|
||||
func vmRunToolingHarnessLaunchScript(repoName string) string {
|
||||
var script strings.Builder
|
||||
script.WriteString("set -euo pipefail\n")
|
||||
fmt.Fprintf(&script, "HELPER=%s\n", shellQuote(vmRunToolingHarnessPath(spec.RepoName)))
|
||||
fmt.Fprintf(&script, "LOG=%s\n", shellQuote(vmRunToolingHarnessLogPath(spec.RepoName)))
|
||||
fmt.Fprintf(&script, "HELPER=%s\n", shellQuote(vmRunToolingHarnessPath(repoName)))
|
||||
fmt.Fprintf(&script, "LOG=%s\n", shellQuote(vmRunToolingHarnessLogPath(repoName)))
|
||||
script.WriteString("mkdir -p \"$(dirname \"$LOG\")\"\n")
|
||||
script.WriteString("nohup bash \"$HELPER\" >\"$LOG\" 2>&1 </dev/null &\n")
|
||||
script.WriteString("disown || true\n")
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
|
||||
"banger/internal/api"
|
||||
"banger/internal/buildinfo"
|
||||
"banger/internal/daemon/workspace"
|
||||
"banger/internal/model"
|
||||
"banger/internal/system"
|
||||
"banger/internal/toolingplan"
|
||||
|
|
@ -1133,111 +1134,21 @@ func TestValidateSSHPrereqsFailsForMissingKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveVMRunSourcePathDefaultsToCWD(t *testing.T) {
|
||||
origCWD := cwdFunc
|
||||
t.Cleanup(func() {
|
||||
cwdFunc = origCWD
|
||||
})
|
||||
// CLI-side git inspection moved to internal/daemon/workspace; the
|
||||
// CLI now runs only a minimal preflight. Those tests live in the
|
||||
// workspace package. What we still guard here is the preflight
|
||||
// policy: reject submodules before the VM is created so the user
|
||||
// gets a fast error instead of an orphaned VM.
|
||||
|
||||
want := t.TempDir()
|
||||
cwdFunc = func() (string, error) {
|
||||
return want, nil
|
||||
}
|
||||
|
||||
got, err := resolveVMRunSourcePath("")
|
||||
if err != nil {
|
||||
t.Fatalf("resolveVMRunSourcePath: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("resolveVMRunSourcePath() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectVMRunRepoUsesRepoRootAndOverlayPaths(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not installed")
|
||||
}
|
||||
|
||||
repoRoot := t.TempDir()
|
||||
globalConfigPath := filepath.Join(t.TempDir(), "global.gitconfig")
|
||||
t.Setenv("GIT_CONFIG_GLOBAL", globalConfigPath)
|
||||
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")
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(repoRoot, "dir"), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(dir): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, ".gitignore"), []byte("ignored.txt\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(.gitignore): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "tracked.txt"), []byte("tracked\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(tracked.txt): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "dir", "keep.txt"), []byte("keep\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(keep.txt): %v", err)
|
||||
}
|
||||
testRunGit(t, repoRoot, "add", ".")
|
||||
testRunGit(t, repoRoot, "commit", "-m", "init")
|
||||
testRunGit(t, repoRoot, "checkout", "-b", "trunk")
|
||||
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "tracked.txt"), []byte("tracked local\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(tracked.txt local): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "untracked.txt"), []byte("untracked\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(untracked.txt): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "ignored.txt"), []byte("ignored\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(ignored.txt): %v", err)
|
||||
}
|
||||
|
||||
spec, err := inspectVMRunRepo(context.Background(), filepath.Join(repoRoot, "dir"), "", "HEAD")
|
||||
if err != nil {
|
||||
t.Fatalf("inspectVMRunRepo: %v", err)
|
||||
}
|
||||
|
||||
if spec.RepoRoot != repoRoot {
|
||||
t.Fatalf("RepoRoot = %q, want %q", spec.RepoRoot, repoRoot)
|
||||
}
|
||||
if spec.RepoName != filepath.Base(repoRoot) {
|
||||
t.Fatalf("RepoName = %q, want %q", spec.RepoName, filepath.Base(repoRoot))
|
||||
}
|
||||
if spec.CurrentBranch != "trunk" {
|
||||
t.Fatalf("CurrentBranch = %q, want trunk", spec.CurrentBranch)
|
||||
}
|
||||
if spec.HeadCommit == "" {
|
||||
t.Fatal("HeadCommit should not be empty")
|
||||
}
|
||||
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)
|
||||
}
|
||||
if spec.GitUserEmail != "test@example.com" {
|
||||
t.Fatalf("GitUserEmail = %q, want test@example.com", spec.GitUserEmail)
|
||||
}
|
||||
wantOverlay := []string{".gitignore", "dir/keep.txt", "tracked.txt", "untracked.txt"}
|
||||
if !reflect.DeepEqual(spec.OverlayPaths, wantOverlay) {
|
||||
t.Fatalf("OverlayPaths = %v, want %v", spec.OverlayPaths, wantOverlay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectVMRunRepoRejectsSubmodules(t *testing.T) {
|
||||
func TestVMRunPreflightRejectsSubmodules(t *testing.T) {
|
||||
repoRoot := t.TempDir()
|
||||
|
||||
origHostCommandOutput := hostCommandOutputFunc
|
||||
origHostCommandOutput := workspace.HostCommandOutputFunc
|
||||
t.Cleanup(func() {
|
||||
hostCommandOutputFunc = origHostCommandOutput
|
||||
workspace.HostCommandOutputFunc = origHostCommandOutput
|
||||
})
|
||||
|
||||
hostCommandOutputFunc = func(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
workspace.HostCommandOutputFunc = func(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
t.Helper()
|
||||
if name != "git" {
|
||||
t.Fatalf("command = %q, want git", name)
|
||||
|
|
@ -1255,9 +1166,9 @@ func TestInspectVMRunRepoRejectsSubmodules(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
_, err := inspectVMRunRepo(context.Background(), repoRoot, "", "HEAD")
|
||||
_, err := vmRunPreflightRepo(context.Background(), repoRoot)
|
||||
if err == nil || !strings.Contains(err.Error(), "submodules") {
|
||||
t.Fatalf("inspectVMRunRepo() error = %v, want submodule rejection", err)
|
||||
t.Fatalf("vmRunPreflightRepo() error = %v, want submodule rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1321,7 +1232,7 @@ func TestRunVMRunWorkspacePreparesAndAttaches(t *testing.T) {
|
|||
var workspaceParams api.VMWorkspacePrepareParams
|
||||
vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
|
||||
workspaceParams = params
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo"}}, nil
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo", RepoName: "repo", RepoRoot: "/tmp/repo"}}, nil
|
||||
}
|
||||
buildVMRunToolingPlanFunc = func(context.Context, string) toolingplan.Plan {
|
||||
return toolingplan.Plan{
|
||||
|
|
@ -1338,10 +1249,7 @@ func TestRunVMRunWorkspacePreparesAndAttaches(t *testing.T) {
|
|||
return api.VMHealthResult{Name: "devbox", Healthy: false}, nil
|
||||
}
|
||||
|
||||
spec := vmRunRepoSpec{
|
||||
SourcePath: repoRoot, RepoRoot: repoRoot, RepoName: "repo",
|
||||
HeadCommit: "deadbeef", CurrentBranch: "main",
|
||||
}
|
||||
repo := vmRunRepo{sourcePath: repoRoot}
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runVMRun(
|
||||
context.Background(),
|
||||
|
|
@ -1350,7 +1258,7 @@ func TestRunVMRunWorkspacePreparesAndAttaches(t *testing.T) {
|
|||
strings.NewReader(""),
|
||||
&stdout, &stderr,
|
||||
api.VMCreateParams{Name: "devbox"},
|
||||
&spec,
|
||||
&repo,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
|
|
@ -1425,7 +1333,7 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
|
|||
return &testVMRunGuestClient{}, nil
|
||||
}
|
||||
vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo"}}, nil
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo", RepoName: "repo", RepoRoot: "/tmp/repo"}}, nil
|
||||
}
|
||||
sshExecFunc = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error {
|
||||
return nil
|
||||
|
|
@ -1434,7 +1342,7 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
|
|||
return api.VMHealthResult{Name: "devbox", Healthy: false}, nil
|
||||
}
|
||||
|
||||
spec := vmRunRepoSpec{RepoRoot: t.TempDir(), RepoName: "repo", HeadCommit: "deadbeef"}
|
||||
repo := vmRunRepo{sourcePath: t.TempDir()}
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runVMRun(
|
||||
context.Background(),
|
||||
|
|
@ -1443,7 +1351,7 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
|
|||
strings.NewReader(""),
|
||||
&stdout, &stderr,
|
||||
api.VMCreateParams{Name: "devbox"},
|
||||
&spec,
|
||||
&repo,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
|
|
@ -1515,7 +1423,7 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
|
|||
return fakeClient, nil
|
||||
}
|
||||
vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo"}}, nil
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo", RepoName: "repo", RepoRoot: "/tmp/repo"}}, nil
|
||||
}
|
||||
sshExecCalls := 0
|
||||
sshExecFunc = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error {
|
||||
|
|
@ -1526,7 +1434,7 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
|
|||
return api.VMHealthResult{Healthy: false}, nil
|
||||
}
|
||||
|
||||
spec := vmRunRepoSpec{RepoRoot: t.TempDir(), RepoName: "repo", HeadCommit: "deadbeef"}
|
||||
repo := vmRunRepo{sourcePath: t.TempDir()}
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runVMRun(
|
||||
context.Background(),
|
||||
|
|
@ -1535,7 +1443,7 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
|
|||
strings.NewReader(""),
|
||||
&stdout, &stderr,
|
||||
api.VMCreateParams{Name: "devbox"},
|
||||
&spec,
|
||||
&repo,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
|
|
@ -1892,7 +1800,7 @@ func TestSplitVMRunArgsPartitionsOnDash(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVMRunToolingHarnessScriptUsesMiseOnly(t *testing.T) {
|
||||
script := vmRunToolingHarnessScript(vmRunRepoSpec{RepoName: "repo"}, toolingplan.Plan{
|
||||
script := vmRunToolingHarnessScript(toolingplan.Plan{
|
||||
RepoManagedTools: []string{"node"},
|
||||
Steps: []toolingplan.InstallStep{{Tool: "go", Version: "1.25.0", Source: "go.mod"}},
|
||||
Skips: []toolingplan.SkipNote{{Target: "python", Reason: "no .python-version"}},
|
||||
|
|
@ -1915,68 +1823,10 @@ func TestVMRunToolingHarnessScriptUsesMiseOnly(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
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",
|
||||
GitUserName: "Repo User",
|
||||
})
|
||||
|
||||
if strings.Contains(script, `git -C "$DIR" config user.name`) || strings.Contains(script, `git -C "$DIR" config user.email`) {
|
||||
t.Fatalf("script = %q, want no repo-local git identity commands", script)
|
||||
}
|
||||
}
|
||||
// The shallow-repo-copy + checkout-script paths used to live in the
|
||||
// CLI. They now live in internal/daemon/workspace and are exercised
|
||||
// by that package's tests; no need to duplicate here.
|
||||
|
||||
func TestVMRunGuestDirIsFixed(t *testing.T) {
|
||||
if got := vmRunGuestDir(); got != "/root/repo" {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue