workspace.export: add base_commit to capture worker git commits

Without base_commit, export diffs against the current guest HEAD.
If the worker ran git commit inside the VM, HEAD advanced and the
diff came back empty — committed work was silently lost.

With base_commit set to the head_commit from workspace.prepare,
the diff uses that fixed point instead. After git add -A the index
holds the full working state, so git diff --cached <base_commit>
captures everything: committed deltas (HEAD moved past base) and
any uncommitted changes on top, in one patch, applied with the
same git apply flow.

- WorkspaceExportParams gains base_commit
- WorkspaceExportResult echoes back the ref actually used
- CLI gains --base-commit flag
- Tests assert scripts use the caller-supplied ref and that
  omitting it falls back to HEAD
This commit is contained in:
Thales Maciel 2026-04-14 16:13:05 -03:00
parent 94c353f317
commit ff51b7ce21
No known key found for this signature in database
GPG key ID: 33112E6833C34679
6 changed files with 162 additions and 10 deletions

View file

@ -941,13 +941,15 @@ func newVMWorkspacePrepareCommand() *cobra.Command {
func newVMWorkspaceExportCommand() *cobra.Command {
var guestPath string
var outputPath string
var baseCommit string
cmd := &cobra.Command{
Use: "export <id-or-name>",
Short: "Pull changes from a guest workspace back to the host as a patch",
Long: "Stage all changes inside the guest workspace (git add -A) and emit a binary-safe unified diff against HEAD. With no --output flag the patch is written to stdout so it can be piped directly to git apply.",
Long: "Stage all changes inside the guest workspace (git add -A) and emit a binary-safe unified diff. Pass --base-commit with the head_commit from workspace prepare to capture changes even when the worker ran git commit inside the VM. Without --base-commit the diff is against the current guest HEAD, which misses committed changes.",
Args: exactArgsUsage(1, "usage: banger vm workspace export <id-or-name>"),
Example: strings.TrimSpace(`
banger vm workspace export devbox | git apply
banger vm workspace export devbox --base-commit abc1234 | git apply
banger vm workspace export devbox --output worker.diff
banger vm workspace export devbox --guest-path /root/project --output changes.diff
`),
@ -957,8 +959,9 @@ func newVMWorkspaceExportCommand() *cobra.Command {
return err
}
result, err := vmWorkspaceExportFunc(cmd.Context(), layout.SocketPath, api.WorkspaceExportParams{
IDOrName: args[0],
GuestPath: guestPath,
IDOrName: args[0],
GuestPath: guestPath,
BaseCommit: baseCommit,
})
if err != nil {
return err
@ -981,6 +984,7 @@ func newVMWorkspaceExportCommand() *cobra.Command {
}
cmd.Flags().StringVar(&guestPath, "guest-path", "/root/repo", "guest workspace path")
cmd.Flags().StringVar(&outputPath, "output", "", "write patch to this file instead of stdout")
cmd.Flags().StringVar(&baseCommit, "base-commit", "", "diff from this commit (use head_commit from workspace prepare to capture worker git commits)")
return cmd
}

View file

@ -2218,3 +2218,30 @@ func TestVMWorkspaceExportGuestPathFlag(t *testing.T) {
t.Fatalf("IDOrName = %q, want devbox", capturedParams.IDOrName)
}
}
func TestVMWorkspaceExportBaseCommitFlag(t *testing.T) {
stubEnsureDaemonForSend(t)
origExport := vmWorkspaceExportFunc
t.Cleanup(func() { vmWorkspaceExportFunc = origExport })
var capturedParams api.WorkspaceExportParams
vmWorkspaceExportFunc = func(_ context.Context, _ string, params api.WorkspaceExportParams) (api.WorkspaceExportResult, error) {
capturedParams = params
return api.WorkspaceExportResult{
HasChanges: false,
BaseCommit: params.BaseCommit,
}, nil
}
cmd := NewBangerCommand()
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"vm", "workspace", "export", "devbox", "--base-commit", "abc1234deadbeef"})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute: %v", err)
}
if capturedParams.BaseCommit != "abc1234deadbeef" {
t.Fatalf("BaseCommit = %q, want abc1234deadbeef", capturedParams.BaseCommit)
}
}