Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions cmd/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import (
"github.com/spf13/cobra"
)

var commitFormat string
var (
commitFormat string
commitName string
commitDescription string
)

// commitCmd is the parent command for commit operations.
// Bare `vers commit` (no args, no subcommand) creates a commit of HEAD for backward compat.
Expand Down Expand Up @@ -42,7 +46,14 @@ var commitCreateCmd = &cobra.Command{
Long: `Save the current state of a VM as a commit.
If no VM ID or alias is provided, commits the current HEAD VM.

Use --format json for machine-readable output.`,
Use --name to give the commit a human-readable name.
Use --description to add additional context.
Use --format json for machine-readable output.

Examples:
vers commit create --name "golden-image-v3"
vers commit create --name "pre-deploy" --description "Before deploying auth changes"
vers commit create vm-123 --name "checkpoint"`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
target := ""
Expand All @@ -54,7 +65,9 @@ Use --format json for machine-readable output.`,
defer cancel()

res, err := handlers.HandleCommitCreate(apiCtx, application, handlers.CommitCreateReq{
Target: target,
Target: target,
Name: commitName,
Description: commitDescription,
})
if err != nil {
return err
Expand All @@ -70,6 +83,12 @@ Use --format json for machine-readable output.`,
}
fmt.Printf("✓ Committed VM '%s'\n", res.VmID)
fmt.Printf("Commit ID: %s\n", res.CommitID)
if res.Name != "" {
fmt.Printf("Name: %s\n", res.Name)
}
if res.Description != "" {
fmt.Printf("Description: %s\n", res.Description)
}
}
return nil
},
Expand Down Expand Up @@ -228,6 +247,8 @@ func init() {
rootCmd.AddCommand(commitCmd)

commitCreateCmd.Flags().StringVar(&commitFormat, "format", "", "Output format (json)")
commitCreateCmd.Flags().StringVarP(&commitName, "name", "n", "", "Human-readable name for the commit")
commitCreateCmd.Flags().StringVarP(&commitDescription, "description", "d", "", "Description for the commit")
commitCmd.AddCommand(commitCreateCmd)

commitListCmd.Flags().BoolVar(&commitListPublic, "public", false, "List public commits instead of your own")
Expand Down
32 changes: 24 additions & 8 deletions internal/handlers/commit_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ import (
"github.com/hdresearch/vers-cli/internal/app"
"github.com/hdresearch/vers-cli/internal/utils"
vers "github.com/hdresearch/vers-sdk-go"
"github.com/hdresearch/vers-sdk-go/option"
)

type CommitCreateReq struct {
Target string
Target string
Name string
Description string
}

type CommitCreateView struct {
CommitID string `json:"commit_id"`
VmID string `json:"vm_id"`
UsedHEAD bool `json:"used_head,omitempty"`
CommitID string `json:"commit_id"`
VmID string `json:"vm_id"`
UsedHEAD bool `json:"used_head,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
}

func HandleCommitCreate(ctx context.Context, a *app.App, r CommitCreateReq) (CommitCreateView, error) {
Expand All @@ -25,14 +30,25 @@ func HandleCommitCreate(ctx context.Context, a *app.App, r CommitCreateReq) (Com
return CommitCreateView{}, err
}

resp, err := a.Client.Vm.Commit(ctx, resolved.ID, vers.VmCommitParams{})
// Build request options to send name/description in the request body
var opts []option.RequestOption
if r.Name != "" {
opts = append(opts, option.WithJSONSet("name", r.Name))
}
if r.Description != "" {
opts = append(opts, option.WithJSONSet("description", r.Description))
}

resp, err := a.Client.Vm.Commit(ctx, resolved.ID, vers.VmCommitParams{}, opts...)
if err != nil {
return CommitCreateView{}, fmt.Errorf("failed to commit VM '%s': %w", resolved.ID, err)
}

return CommitCreateView{
CommitID: resp.CommitID,
VmID: resolved.ID,
UsedHEAD: resolved.UsedHEAD,
CommitID: resp.CommitID,
VmID: resolved.ID,
UsedHEAD: resolved.UsedHEAD,
Name: r.Name,
Description: r.Description,
}, nil
}
153 changes: 153 additions & 0 deletions internal/handlers/commit_create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package handlers_test

import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/hdresearch/vers-cli/internal/handlers"
)

func TestHandleCommitCreate_WithName(t *testing.T) {
var commitBody map[string]interface{}

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/vm/vm-123/status":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"vm_id":"vm-123","owner_id":"owner-1","created_at":"2026-01-01T00:00:00Z","state":"running"}`))

case r.Method == http.MethodPost && r.URL.Path == "/api/v1/vm/vm-123/commit":
body, _ := io.ReadAll(r.Body)
json.Unmarshal(body, &commitBody)
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"commit_id":"commit-abc"}`))

default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()

a := testApp(server.URL)
res, err := handlers.HandleCommitCreate(context.Background(), a, handlers.CommitCreateReq{
Target: "vm-123",
Name: "my-commit",
Description: "my description",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.CommitID != "commit-abc" {
t.Errorf("expected commit ID commit-abc, got %s", res.CommitID)
}
if res.VmID != "vm-123" {
t.Errorf("expected VM ID vm-123, got %s", res.VmID)
}
if res.Name != "my-commit" {
t.Errorf("expected name my-commit, got %s", res.Name)
}
if res.Description != "my description" {
t.Errorf("expected description 'my description', got %s", res.Description)
}

// Verify name and description were sent in the commit request body
if commitBody == nil {
t.Fatal("expected commit request to have a body")
}
if commitBody["name"] != "my-commit" {
t.Errorf("expected body name=my-commit, got %v", commitBody["name"])
}
if commitBody["description"] != "my description" {
t.Errorf("expected body description='my description', got %v", commitBody["description"])
}
}

func TestHandleCommitCreate_WithoutName(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/vm/vm-123/status":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"vm_id":"vm-123","owner_id":"owner-1","created_at":"2026-01-01T00:00:00Z","state":"running"}`))

case r.Method == http.MethodPost && r.URL.Path == "/api/v1/vm/vm-123/commit":
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"commit_id":"commit-abc"}`))

default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()

a := testApp(server.URL)
res, err := handlers.HandleCommitCreate(context.Background(), a, handlers.CommitCreateReq{
Target: "vm-123",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.CommitID != "commit-abc" {
t.Errorf("expected commit ID commit-abc, got %s", res.CommitID)
}
if res.Name != "" {
t.Errorf("expected empty name, got %s", res.Name)
}
}

func TestHandleCommitCreate_NameOnly(t *testing.T) {
var commitBody map[string]interface{}

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/vm/vm-123/status":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"vm_id":"vm-123","owner_id":"owner-1","created_at":"2026-01-01T00:00:00Z","state":"running"}`))

case r.Method == http.MethodPost && r.URL.Path == "/api/v1/vm/vm-123/commit":
body, _ := io.ReadAll(r.Body)
json.Unmarshal(body, &commitBody)
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"commit_id":"commit-abc"}`))

default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()

a := testApp(server.URL)
res, err := handlers.HandleCommitCreate(context.Background(), a, handlers.CommitCreateReq{
Target: "vm-123",
Name: "just-a-name",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.Name != "just-a-name" {
t.Errorf("expected name just-a-name, got %s", res.Name)
}
if res.Description != "" {
t.Errorf("expected empty description, got %s", res.Description)
}

// Verify name was sent but description was not
if commitBody["name"] != "just-a-name" {
t.Errorf("expected name in body, got %v", commitBody["name"])
}
if _, hasDesc := commitBody["description"]; hasDesc {
t.Error("description should not be in body when not provided")
}
}
Loading