Skip to content

Commit 8bb4560

Browse files
Merge pull request #183 from hdresearch/fix/commit-create-name-description
feat: add --name and --description flags to `commit create`
2 parents bf6e3a3 + d9d4a30 commit 8bb4560

3 files changed

Lines changed: 202 additions & 12 deletions

File tree

cmd/commit.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ import (
99
"github.com/spf13/cobra"
1010
)
1111

12-
var commitJSON bool
13-
var commitFormat string
12+
var (
13+
commitJSON bool
14+
commitFormat string
15+
commitName string
16+
commitDescription string
17+
)
1418

1519
// commitCmd is the parent command for commit operations.
1620
// Bare `vers commit` (no args, no subcommand) creates a commit of HEAD for backward compat.
@@ -43,7 +47,14 @@ var commitCreateCmd = &cobra.Command{
4347
Long: `Save the current state of a VM as a commit.
4448
If no VM ID or alias is provided, commits the current HEAD VM.
4549
46-
Use --json for machine-readable output.`,
50+
Use --name to give the commit a human-readable name.
51+
Use --description to add additional context.
52+
Use --json for machine-readable output.
53+
54+
Examples:
55+
vers commit create --name "golden-image-v3"
56+
vers commit create --name "pre-deploy" --description "Before deploying auth changes"
57+
vers commit create vm-123 --name "checkpoint"`,
4758
Args: cobra.MaximumNArgs(1),
4859
RunE: func(cmd *cobra.Command, args []string) error {
4960
target := ""
@@ -55,7 +66,9 @@ Use --json for machine-readable output.`,
5566
defer cancel()
5667

5768
res, err := handlers.HandleCommitCreate(apiCtx, application, handlers.CommitCreateReq{
58-
Target: target,
69+
Target: target,
70+
Name: commitName,
71+
Description: commitDescription,
5972
})
6073
if err != nil {
6174
return err
@@ -74,6 +87,12 @@ Use --json for machine-readable output.`,
7487
}
7588
fmt.Printf("Committed VM '%s'\n", res.VmID)
7689
fmt.Printf("Commit ID: %s\n", res.CommitID)
90+
if res.Name != "" {
91+
fmt.Printf("Name: %s\n", res.Name)
92+
}
93+
if res.Description != "" {
94+
fmt.Printf("Description: %s\n", res.Description)
95+
}
7796
}
7897
return nil
7998
},
@@ -263,6 +282,8 @@ func init() {
263282
commitCreateCmd.Flags().BoolVar(&commitJSON, "json", false, "Output as JSON")
264283
commitCreateCmd.Flags().StringVar(&commitFormat, "format", "", "Output format (json) [deprecated: use --json]")
265284
_ = commitCreateCmd.Flags().MarkDeprecated("format", "use --json instead")
285+
commitCreateCmd.Flags().StringVarP(&commitName, "name", "n", "", "Human-readable name for the commit")
286+
commitCreateCmd.Flags().StringVarP(&commitDescription, "description", "d", "", "Description for the commit")
266287
commitCmd.AddCommand(commitCreateCmd)
267288

268289
commitListCmd.Flags().BoolVar(&commitListPublic, "public", false, "List public commits instead of your own")

internal/handlers/commit_create.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,21 @@ import (
77
"github.com/hdresearch/vers-cli/internal/app"
88
"github.com/hdresearch/vers-cli/internal/utils"
99
vers "github.com/hdresearch/vers-sdk-go"
10+
"github.com/hdresearch/vers-sdk-go/option"
1011
)
1112

1213
type CommitCreateReq struct {
13-
Target string
14+
Target string
15+
Name string
16+
Description string
1417
}
1518

1619
type CommitCreateView struct {
17-
CommitID string `json:"commit_id"`
18-
VmID string `json:"vm_id"`
19-
UsedHEAD bool `json:"used_head,omitempty"`
20+
CommitID string `json:"commit_id"`
21+
VmID string `json:"vm_id"`
22+
UsedHEAD bool `json:"used_head,omitempty"`
23+
Name string `json:"name,omitempty"`
24+
Description string `json:"description,omitempty"`
2025
}
2126

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

28-
resp, err := a.Client.Vm.Commit(ctx, resolved.ID, vers.VmCommitParams{})
33+
// Build request options to send name/description in the request body
34+
var opts []option.RequestOption
35+
if r.Name != "" {
36+
opts = append(opts, option.WithJSONSet("name", r.Name))
37+
}
38+
if r.Description != "" {
39+
opts = append(opts, option.WithJSONSet("description", r.Description))
40+
}
41+
42+
resp, err := a.Client.Vm.Commit(ctx, resolved.ID, vers.VmCommitParams{}, opts...)
2943
if err != nil {
3044
return CommitCreateView{}, fmt.Errorf("failed to commit VM '%s': %w", resolved.ID, err)
3145
}
3246

3347
return CommitCreateView{
34-
CommitID: resp.CommitID,
35-
VmID: resolved.ID,
36-
UsedHEAD: resolved.UsedHEAD,
48+
CommitID: resp.CommitID,
49+
VmID: resolved.ID,
50+
UsedHEAD: resolved.UsedHEAD,
51+
Name: r.Name,
52+
Description: r.Description,
3753
}, nil
3854
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package handlers_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/hdresearch/vers-cli/internal/handlers"
12+
)
13+
14+
func TestHandleCommitCreate_WithName(t *testing.T) {
15+
var commitBody map[string]interface{}
16+
17+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18+
w.Header().Set("Content-Type", "application/json")
19+
20+
switch {
21+
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/vm/vm-123/status":
22+
w.WriteHeader(http.StatusOK)
23+
w.Write([]byte(`{"vm_id":"vm-123","owner_id":"owner-1","created_at":"2026-01-01T00:00:00Z","state":"running"}`))
24+
25+
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/vm/vm-123/commit":
26+
body, _ := io.ReadAll(r.Body)
27+
json.Unmarshal(body, &commitBody)
28+
w.WriteHeader(http.StatusCreated)
29+
w.Write([]byte(`{"commit_id":"commit-abc"}`))
30+
31+
default:
32+
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
33+
w.WriteHeader(http.StatusNotFound)
34+
}
35+
}))
36+
defer server.Close()
37+
38+
a := testApp(server.URL)
39+
res, err := handlers.HandleCommitCreate(context.Background(), a, handlers.CommitCreateReq{
40+
Target: "vm-123",
41+
Name: "my-commit",
42+
Description: "my description",
43+
})
44+
if err != nil {
45+
t.Fatalf("unexpected error: %v", err)
46+
}
47+
if res.CommitID != "commit-abc" {
48+
t.Errorf("expected commit ID commit-abc, got %s", res.CommitID)
49+
}
50+
if res.VmID != "vm-123" {
51+
t.Errorf("expected VM ID vm-123, got %s", res.VmID)
52+
}
53+
if res.Name != "my-commit" {
54+
t.Errorf("expected name my-commit, got %s", res.Name)
55+
}
56+
if res.Description != "my description" {
57+
t.Errorf("expected description 'my description', got %s", res.Description)
58+
}
59+
60+
// Verify name and description were sent in the commit request body
61+
if commitBody == nil {
62+
t.Fatal("expected commit request to have a body")
63+
}
64+
if commitBody["name"] != "my-commit" {
65+
t.Errorf("expected body name=my-commit, got %v", commitBody["name"])
66+
}
67+
if commitBody["description"] != "my description" {
68+
t.Errorf("expected body description='my description', got %v", commitBody["description"])
69+
}
70+
}
71+
72+
func TestHandleCommitCreate_WithoutName(t *testing.T) {
73+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74+
w.Header().Set("Content-Type", "application/json")
75+
76+
switch {
77+
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/vm/vm-123/status":
78+
w.WriteHeader(http.StatusOK)
79+
w.Write([]byte(`{"vm_id":"vm-123","owner_id":"owner-1","created_at":"2026-01-01T00:00:00Z","state":"running"}`))
80+
81+
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/vm/vm-123/commit":
82+
w.WriteHeader(http.StatusCreated)
83+
w.Write([]byte(`{"commit_id":"commit-abc"}`))
84+
85+
default:
86+
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
87+
w.WriteHeader(http.StatusNotFound)
88+
}
89+
}))
90+
defer server.Close()
91+
92+
a := testApp(server.URL)
93+
res, err := handlers.HandleCommitCreate(context.Background(), a, handlers.CommitCreateReq{
94+
Target: "vm-123",
95+
})
96+
if err != nil {
97+
t.Fatalf("unexpected error: %v", err)
98+
}
99+
if res.CommitID != "commit-abc" {
100+
t.Errorf("expected commit ID commit-abc, got %s", res.CommitID)
101+
}
102+
if res.Name != "" {
103+
t.Errorf("expected empty name, got %s", res.Name)
104+
}
105+
}
106+
107+
func TestHandleCommitCreate_NameOnly(t *testing.T) {
108+
var commitBody map[string]interface{}
109+
110+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
111+
w.Header().Set("Content-Type", "application/json")
112+
113+
switch {
114+
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/vm/vm-123/status":
115+
w.WriteHeader(http.StatusOK)
116+
w.Write([]byte(`{"vm_id":"vm-123","owner_id":"owner-1","created_at":"2026-01-01T00:00:00Z","state":"running"}`))
117+
118+
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/vm/vm-123/commit":
119+
body, _ := io.ReadAll(r.Body)
120+
json.Unmarshal(body, &commitBody)
121+
w.WriteHeader(http.StatusCreated)
122+
w.Write([]byte(`{"commit_id":"commit-abc"}`))
123+
124+
default:
125+
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
126+
w.WriteHeader(http.StatusNotFound)
127+
}
128+
}))
129+
defer server.Close()
130+
131+
a := testApp(server.URL)
132+
res, err := handlers.HandleCommitCreate(context.Background(), a, handlers.CommitCreateReq{
133+
Target: "vm-123",
134+
Name: "just-a-name",
135+
})
136+
if err != nil {
137+
t.Fatalf("unexpected error: %v", err)
138+
}
139+
if res.Name != "just-a-name" {
140+
t.Errorf("expected name just-a-name, got %s", res.Name)
141+
}
142+
if res.Description != "" {
143+
t.Errorf("expected empty description, got %s", res.Description)
144+
}
145+
146+
// Verify name was sent but description was not
147+
if commitBody["name"] != "just-a-name" {
148+
t.Errorf("expected name in body, got %v", commitBody["name"])
149+
}
150+
if _, hasDesc := commitBody["description"]; hasDesc {
151+
t.Error("description should not be in body when not provided")
152+
}
153+
}

0 commit comments

Comments
 (0)