From 58f7bcdb349edf597108eda7e7e9901da6021242 Mon Sep 17 00:00:00 2001 From: Tynan Daly Date: Mon, 11 May 2026 21:41:33 -0700 Subject: [PATCH] feat(commit): add --tag and --public flags to commit create (closes #201) --- README.md | 6 + cmd/commit.go | 23 +- internal/handlers/commit_create.go | 150 ++++++++++++- internal/handlers/commit_create_test.go | 254 +++++++++++++++++++++++ internal/presenters/commits_presenter.go | 25 +++ internal/presenters/commits_types.go | 19 ++ 6 files changed, 454 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 11c59e7..90ad445 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,12 @@ vers commit # Commit a specific VM vers commit +# Commit + tag + publish in one shot +vers commit create --tag my-app:v1.2 --tag my-app:latest --public +# --tag : (repeatable) creates the tag, or updates an existing one +# to point at the new commit +# --public publishes the new commit (is_public=true) + # List your commits vers commit list vers commit list -q # just IDs diff --git a/cmd/commit.go b/cmd/commit.go index b9ac039..889a2c5 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -14,6 +14,8 @@ var ( commitFormat string commitName string commitDescription string + commitTags []string + commitPublic bool ) // commitCmd is the parent command for commit operations. @@ -49,12 +51,15 @@ If no VM ID or alias is provided, commits the current HEAD VM. Use --name to give the commit a human-readable name. Use --description to add additional context. +Use --tag : (repeatable) to create or update repo tags pointing at the new commit. +Use --public to publish the commit (set is_public=true) after it lands. Use --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"`, + vers commit create vm-123 --name "checkpoint" + vers commit create vm-123 --tag my-app:v1.2 --tag my-app:latest --public`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { target := "" @@ -69,6 +74,8 @@ Examples: Target: target, Name: commitName, Description: commitDescription, + Tags: commitTags, + Public: commitPublic, }) if err != nil { return err @@ -82,17 +89,7 @@ Examples: case pres.FormatJSON: pres.PrintJSON(res) default: - if res.UsedHEAD { - fmt.Printf("Using current HEAD VM: %s\n", res.VmID) - } - 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) - } + pres.RenderCommitCreate(application, res) } return nil }, @@ -284,6 +281,8 @@ func init() { _ = commitCreateCmd.Flags().MarkDeprecated("format", "use --json instead") commitCreateCmd.Flags().StringVarP(&commitName, "name", "n", "", "Human-readable name for the commit") commitCreateCmd.Flags().StringVarP(&commitDescription, "description", "d", "", "Description for the commit") + commitCreateCmd.Flags().StringSliceVar(&commitTags, "tag", nil, "Repo tag to write pointing at the new commit, in : form (repeatable)") + commitCreateCmd.Flags().BoolVar(&commitPublic, "public", false, "Publish the commit (set is_public=true) after it lands") commitCmd.AddCommand(commitCreateCmd) commitListCmd.Flags().BoolVar(&commitListPublic, "public", false, "List public commits instead of your own") diff --git a/internal/handlers/commit_create.go b/internal/handlers/commit_create.go index 6fa3a46..3529590 100644 --- a/internal/handlers/commit_create.go +++ b/internal/handlers/commit_create.go @@ -2,9 +2,14 @@ package handlers import ( "context" + "errors" "fmt" + "io" + "net/http" + "strings" "github.com/hdresearch/vers-cli/internal/app" + "github.com/hdresearch/vers-cli/internal/presenters" "github.com/hdresearch/vers-cli/internal/utils" vers "github.com/hdresearch/vers-sdk-go" "github.com/hdresearch/vers-sdk-go/option" @@ -14,23 +19,73 @@ type CommitCreateReq struct { Target string Name string Description string + // Tags is a list of raw ":" references to write after the + // commit lands. Each entry creates the tag if it does not yet exist, + // or updates an existing tag to point at the new commit. + Tags []string + // Public, when true, publishes the new commit (sets is_public=true) + // after the commit and any tag writes succeed. + Public bool } -type CommitCreateView struct { - 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"` +// CommitCreateView is re-exported from the presenters package so callers +// can keep importing it from handlers as before. +type CommitCreateView = presenters.CommitCreateView + +// CommitTagWritten is re-exported for the same reason. +type CommitTagWritten = presenters.CommitTagWritten + +// parsed form of a single --tag value +type tagSpec struct { + reference string // original "repo:tag" input + repo string + tag string +} + +func parseTagSpecs(raw []string) ([]tagSpec, error) { + specs := make([]tagSpec, 0, len(raw)) + for _, s := range raw { + parts := strings.SplitN(s, ":", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return nil, fmt.Errorf("--tag must be in : form (got: %q)", s) + } + specs = append(specs, tagSpec{reference: s, repo: parts[0], tag: parts[1]}) + } + return specs, nil } func HandleCommitCreate(ctx context.Context, a *app.App, r CommitCreateReq) (CommitCreateView, error) { + // 1. Validate --tag shape up-front, before any side effects. + specs, err := parseTagSpecs(r.Tags) + if err != nil { + return CommitCreateView{}, err + } + + // 2. Verify every referenced repo exists and capture its visibility. + // Fail-fast before creating the commit if any repo is missing. + repoPublic := make(map[string]bool, len(specs)) + for _, s := range specs { + if _, seen := repoPublic[s.repo]; seen { + continue + } + info, err := a.Client.Repositories.Get(ctx, s.repo) + if err != nil { + var apiErr *vers.Error + if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { + return CommitCreateView{}, fmt.Errorf("repo %q not found. Create it first with: vers repo create %s", s.repo, s.repo) + } + return CommitCreateView{}, fmt.Errorf("failed to look up repo %q: %w", s.repo, err) + } + repoPublic[s.repo] = info.IsPublic + } + + // 3. Resolve target VM. resolved, err := utils.ResolveTargetVM(ctx, a.Client, r.Target) if err != nil { return CommitCreateView{}, err } - // Build request options to send name/description in the request body + // 4. Create the commit. var opts []option.RequestOption if r.Name != "" { opts = append(opts, option.WithJSONSet("name", r.Name)) @@ -38,17 +93,90 @@ func HandleCommitCreate(ctx context.Context, a *app.App, r CommitCreateReq) (Com 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) } + commitID := resp.CommitID - return CommitCreateView{ - CommitID: resp.CommitID, + view := presenters.CommitCreateView{ + CommitID: commitID, VmID: resolved.ID, UsedHEAD: resolved.UsedHEAD, Name: r.Name, Description: r.Description, - }, nil + } + + // 5. Write tags. Any failure here returns an error that names the new + // commit ID so the user can recover by hand. + for _, s := range specs { + existing, getErr := a.Client.Repositories.GetTag(ctx, s.repo, s.tag) + var apiErr *vers.Error + switch { + case getErr == nil && existing != nil: + // Tag exists -> update it to point at the new commit. + updErr := HandleRepoTagUpdate(ctx, a, RepoTagUpdateReq{ + RepoName: s.repo, + TagName: s.tag, + CommitID: commitID, + }) + if updErr != nil { + return view, fmt.Errorf("commit %s created, but failed to update tag %s: %w", commitID, s.reference, updErr) + } + view.TagsWritten = append(view.TagsWritten, presenters.CommitTagWritten{ + Reference: s.reference, + TagID: existing.TagID, + }) + case errors.As(getErr, &apiErr) && apiErr.StatusCode == http.StatusNotFound: + // Tag does not exist -> create it pointing at the new commit. + created, createErr := HandleRepoTagCreate(ctx, a, RepoTagCreateReq{ + RepoName: s.repo, + TagName: s.tag, + CommitID: commitID, + }) + if createErr != nil { + return view, fmt.Errorf("commit %s created, but failed to create tag %s: %w", commitID, s.reference, createErr) + } + view.TagsWritten = append(view.TagsWritten, presenters.CommitTagWritten{ + Reference: s.reference, + TagID: created.TagID, + }) + default: + return view, fmt.Errorf("commit %s created, but failed to look up tag %s: %w", commitID, s.reference, getErr) + } + } + + // 6. Publish if requested. + if r.Public { + info, pubErr := HandleCommitUpdate(ctx, a, CommitUpdateReq{ + CommitID: commitID, + IsPublic: true, + }) + if pubErr != nil { + return view, fmt.Errorf("commit %s created (tags written: %d), but failed to publish: %w", commitID, len(view.TagsWritten), pubErr) + } + view.IsPublic = info.IsPublic + } + + // 7. Visibility-mismatch warning: any tag target repo is public but + // the commit was not explicitly published. Stderr only — do not + // auto-publish (the conservative recommendation from #201). + if !r.Public { + var publicRepos []string + for repo, pub := range repoPublic { + if pub { + publicRepos = append(publicRepos, repo) + } + } + if len(publicRepos) > 0 { + out := a.IO.Err + if out == nil { + out = io.Discard + } + fmt.Fprintf(out, "warning: commit %s was tagged into public repo(s) %s but was not published. The tag references a private commit and will not be reachable. Re-run with --public next time, or run: vers commit publish %s\n", + commitID, strings.Join(publicRepos, ", "), commitID) + } + } + + return view, nil } diff --git a/internal/handlers/commit_create_test.go b/internal/handlers/commit_create_test.go index bafadfa..817dca7 100644 --- a/internal/handlers/commit_create_test.go +++ b/internal/handlers/commit_create_test.go @@ -1,16 +1,34 @@ package handlers_test import ( + "bytes" "context" "encoding/json" "io" "net/http" "net/http/httptest" + "strings" "testing" + "github.com/hdresearch/vers-cli/internal/app" "github.com/hdresearch/vers-cli/internal/handlers" + vers "github.com/hdresearch/vers-sdk-go" + "github.com/hdresearch/vers-sdk-go/option" ) +// testAppWithStderr returns an App pointed at baseURL with a captured +// stderr writer, for testing handler warnings. +func testAppWithStderr(baseURL string, stderr io.Writer) *app.App { + client := vers.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("test-key"), + ) + return &app.App{ + Client: client, + IO: app.Output{In: nil, Out: io.Discard, Err: stderr}, + } +} + func TestHandleCommitCreate_WithName(t *testing.T) { var commitBody map[string]interface{} @@ -151,3 +169,239 @@ func TestHandleCommitCreate_NameOnly(t *testing.T) { t.Error("description should not be in body when not provided") } } + +// ── --tag + --public tests ─────────────────────────────────────────── + +func TestHandleCommitCreate_TagBadFormat(t *testing.T) { + // No server should be hit: validation runs before any side effect. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + a := testApp(server.URL) + _, err := handlers.HandleCommitCreate(context.Background(), a, handlers.CommitCreateReq{ + Target: "vm-123", + Tags: []string{"v1.2"}, + }) + if err == nil { + t.Fatal("expected error for bad --tag format") + } + want := `--tag must be in : form (got: "v1.2")` + if !strings.Contains(err.Error(), want) { + t.Errorf("expected error to contain %q, got %q", want, err.Error()) + } +} + +func TestHandleCommitCreate_TagRepoNotFound(t *testing.T) { + var commitCalled bool + 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/repositories/nope": + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"error":"not found"}`)) + case r.URL.Path == "/api/v1/vm/vm-123/commit": + commitCalled = true + 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) + _, err := handlers.HandleCommitCreate(context.Background(), a, handlers.CommitCreateReq{ + Target: "vm-123", + Tags: []string{"nope:v1"}, + }) + if err == nil { + t.Fatal("expected error for missing repo") + } + want := `repo "nope" not found. Create it first with: vers repo create nope` + if !strings.Contains(err.Error(), want) { + t.Errorf("expected error to contain %q, got %q", want, err.Error()) + } + if commitCalled { + t.Error("commit endpoint must not be called when repo lookup fails") + } +} + +func TestHandleCommitCreate_TagCreateNew(t *testing.T) { + var createTagBody 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/repositories/my-app": + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"name":"my-app","repo_id":"repo-1","is_public":false,"created_at":"2026-01-01T00:00:00Z","description":null}`)) + 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"}`)) + case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repositories/my-app/tags/v1": + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"error":"not found"}`)) + case r.Method == http.MethodPost && r.URL.Path == "/api/v1/repositories/my-app/tags": + body, _ := io.ReadAll(r.Body) + json.Unmarshal(body, &createTagBody) + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"commit_id":"commit-abc","reference":"my-app:v1","tag_id":"tag-new"}`)) + 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", + Tags: []string{"my-app:v1"}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res.TagsWritten) != 1 { + t.Fatalf("expected 1 tag written, got %d", len(res.TagsWritten)) + } + got := res.TagsWritten[0] + if got.Reference != "my-app:v1" || got.TagID != "tag-new" { + t.Errorf("unexpected tag written: %+v", got) + } + if createTagBody["tag_name"] != "v1" || createTagBody["commit_id"] != "commit-abc" { + t.Errorf("unexpected create-tag body: %+v", createTagBody) + } + if res.IsPublic { + t.Error("expected IsPublic=false when --public not supplied") + } +} + +func TestHandleCommitCreate_TagUpdateExisting(t *testing.T) { + var updateBody 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/repositories/my-app": + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"name":"my-app","repo_id":"repo-1","is_public":false,"created_at":"2026-01-01T00:00:00Z","description":null}`)) + 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-new"}`)) + case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repositories/my-app/tags/latest": + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"commit_id":"commit-old","reference":"my-app:latest","tag_id":"tag-existing","tag_name":"latest","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z","description":null}`)) + case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/repositories/my-app/tags/latest": + body, _ := io.ReadAll(r.Body) + json.Unmarshal(body, &updateBody) + w.WriteHeader(http.StatusNoContent) + 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", + Tags: []string{"my-app:latest"}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res.TagsWritten) != 1 || res.TagsWritten[0].TagID != "tag-existing" { + t.Fatalf("expected reuse of existing tag_id, got %+v", res.TagsWritten) + } + if updateBody["commit_id"] != "commit-new" { + t.Errorf("expected PATCH to set commit_id=commit-new, got %+v", updateBody) + } +} + +func TestHandleCommitCreate_PublicTriggersPublish(t *testing.T) { + var patchBody 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": + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"commit_id":"commit-abc"}`)) + case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/commits/commit-abc": + body, _ := io.ReadAll(r.Body) + json.Unmarshal(body, &patchBody) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"commit_id":"commit-abc","name":"","owner_id":"owner-1","created_at":"2026-01-01T00:00:00Z","is_public":true}`)) + 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", + Public: true, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !res.IsPublic { + t.Error("expected IsPublic=true after --public") + } + if patchBody["is_public"] != true { + t.Errorf("expected publish PATCH with is_public=true, got %+v", patchBody) + } +} + +func TestHandleCommitCreate_PublicRepoWarning(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/repositories/my-app": + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"name":"my-app","repo_id":"repo-1","is_public":true,"created_at":"2026-01-01T00:00:00Z","description":null}`)) + 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"}`)) + case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repositories/my-app/tags/v1": + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"error":"not found"}`)) + case r.Method == http.MethodPost && r.URL.Path == "/api/v1/repositories/my-app/tags": + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"commit_id":"commit-abc","reference":"my-app:v1","tag_id":"tag-new"}`)) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + var stderr bytes.Buffer + a := testAppWithStderr(server.URL, &stderr) + _, err := handlers.HandleCommitCreate(context.Background(), a, handlers.CommitCreateReq{ + Target: "vm-123", + Tags: []string{"my-app:v1"}, + // no --public + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stderr.String(), "warning:") || !strings.Contains(stderr.String(), "my-app") || !strings.Contains(stderr.String(), "--public") { + t.Errorf("expected stderr to contain visibility warning, got %q", stderr.String()) + } +} diff --git a/internal/presenters/commits_presenter.go b/internal/presenters/commits_presenter.go index 31779a1..4a81390 100644 --- a/internal/presenters/commits_presenter.go +++ b/internal/presenters/commits_presenter.go @@ -6,6 +6,31 @@ import ( "github.com/hdresearch/vers-cli/internal/app" ) +// RenderCommitCreate prints the text-mode summary for a `vers commit create` +// invocation. JSON output is handled separately by pres.PrintJSON. +func RenderCommitCreate(_ *app.App, v CommitCreateView) { + if v.UsedHEAD { + fmt.Printf("Using current HEAD VM: %s\n", v.VmID) + } + fmt.Printf("Committed VM '%s'\n", v.VmID) + fmt.Printf("Commit ID: %s\n", v.CommitID) + if v.Name != "" { + fmt.Printf("Name: %s\n", v.Name) + } + if v.Description != "" { + fmt.Printf("Description: %s\n", v.Description) + } + if len(v.TagsWritten) > 0 { + fmt.Println("Tags written:") + for _, t := range v.TagsWritten { + fmt.Printf(" %s (tag_id: %s)\n", t.Reference, t.TagID) + } + } + if v.IsPublic { + fmt.Println("Visibility: public") + } +} + func RenderCommitsList(_ *app.App, v CommitsListView) { if v.Public { fmt.Println("Public Commits") diff --git a/internal/presenters/commits_types.go b/internal/presenters/commits_types.go index 98d305b..efed196 100644 --- a/internal/presenters/commits_types.go +++ b/internal/presenters/commits_types.go @@ -12,3 +12,22 @@ type CommitParentsView struct { CommitID string Parents []vers.CommitListParentsResponse } + +// CommitTagWritten is one entry in CommitCreateView.TagsWritten. +type CommitTagWritten struct { + Reference string `json:"reference"` + TagID string `json:"tag_id"` +} + +// CommitCreateView is the JSON/text-mode shape for `vers commit create`. +type CommitCreateView struct { + 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"` + TagsWritten []CommitTagWritten `json:"tags_written,omitempty"` + // IsPublic reflects the commit's visibility after any --public publish + // step. Only populated when --public was supplied; otherwise omitted. + IsPublic bool `json:"is_public,omitempty"` +}