diff --git a/internal/app/artifacts.go b/internal/app/artifacts.go index 0b1ed319..20b55e28 100644 --- a/internal/app/artifacts.go +++ b/internal/app/artifacts.go @@ -25,10 +25,8 @@ import ( func (a *application) artifactCommand() *cobra.Command { group := &cobra.Command{ Use: "artifact", - Short: "List and download the artifacts of runs", - Long: `List the artifacts that a run uploaded and download one as a ZIP archive. - -To download and extract the artifacts of a run by name, use tea run download.`, + Short: "List the artifacts of runs", + Long: "List the artifacts that a run uploaded. To download them, use tea run download.", } var limit, page int list := &cobra.Command{ @@ -55,55 +53,6 @@ $ tea run artifact list 34`, list.ValidArgsFunction = a.completeRunArg() group.AddCommand(list) - var destination downloadFlags - download := &cobra.Command{ - Use: "download ", - Short: "Download an artifact as a ZIP archive", - Long: "Download an artifact as a ZIP archive without extracting it, named after the artifact by default.", - Example: `# download artifact 7 to .zip -$ tea run artifact download 7 -# write it to standard output -$ tea run artifact download 7 --output - > build.zip`, - Annotations: textOutput("artifact downloads are binary"), - Args: argsExact(1), - RunE: func(cmd *cobra.Command, args []string) error { - id, err := number(args[0]) - if err != nil { - return err - } - t, err := a.resolve(cmd.Context(), true) - if err != nil { - return err - } - service := services.Actions{Client: t.client, Repository: t.Repo} - name := "" - if destination.output == "" || isDirectory(destination.output) { - artifact, err := service.Artifact(cmd.Context(), id) - if err != nil { - return err - } - name = artifact.Name + ".zip" - } - path, skip, err := destination.destination(name) - if err != nil || skip { - return err - } - body, err := service.DownloadArtifact(cmd.Context(), id) - if err != nil { - return err - } - defer body.Close() - if err := a.download(cmd, destination, path, body); err != nil { - return err - } - if path != "-" { - a.done(cmd, "Downloaded artifact %d to %s", id, path) - } - return nil - }, - } - destination.register(download) - group.AddCommand(download) return group } diff --git a/internal/app/artifacts_test.go b/internal/app/artifacts_test.go index d64cde84..6111d630 100644 --- a/internal/app/artifacts_test.go +++ b/internal/app/artifacts_test.go @@ -16,13 +16,11 @@ import ( "github.com/stretchr/testify/require" ) -func TestArtifactDownloadsValidateDestinationAndKeepBinaryOutputClean(t *testing.T) { - requests := 0 +func TestRunDownloadZipToStdoutKeepsBinaryOutputClean(t *testing.T) { server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - requests++ switch r.URL.Path { - case "/api/v1/repos/owner/repo/actions/artifacts/9": - _, _ = w.Write([]byte(`{"id":9,"name":"build"}`)) + case "/api/v1/repos/owner/repo/actions/runs/29/artifacts": + _, _ = w.Write([]byte(`{"total_count":1,"artifacts":[{"id":9,"name":"build"}]}`)) case "/api/v1/repos/owner/repo/actions/artifacts/9/zip": require.Equal(t, "token token", r.Header.Get("Authorization")) http.Redirect(w, r, "/signed?signature=private", http.StatusFound) @@ -36,27 +34,11 @@ func TestArtifactDownloadsValidateDestinationAndKeepBinaryOutputClean(t *testing })) defer server.Close() deps, out, diagnostics := serverDeps(t, server.URL) - require.NoError(t, Execute(context.Background(), deps, []string{"run", "artifact", "download", "9", "-R", "owner/repo", "--output-file", "-"})) + require.NoError(t, Execute(context.Background(), deps, []string{"run", "download", "29", "-R", "owner/repo", "--zip", "-n", "build", "-O", "-"})) require.Equal(t, []byte{0x50, 0x4b, 0, 0xff}, out.Bytes()) require.Empty(t, diagnostics.String()) - - dir := t.TempDir() - out.Reset() - args := []string{"run", "artifact", "download", "#9", "-R", "owner/repo", "--dir", dir} - require.NoError(t, Execute(context.Background(), deps, args)) - data, err := os.ReadFile(filepath.Join(dir, "build.zip")) - require.NoError(t, err) - require.Equal(t, []byte{0x50, 0x4b, 0, 0xff}, data) - require.Empty(t, out.String()) - before := requests - err = Execute(context.Background(), deps, append(args, "--output", filepath.Join(dir, "build.zip"))) - require.ErrorContains(t, err, "--output and --dir cannot be combined") - err = Execute(context.Background(), deps, []string{"run", "artifact", "download", "9", "-R", "owner/repo", "--output", filepath.Join(dir, "build.zip")}) - require.ErrorContains(t, err, "already exists; pass --clobber") - require.Equal(t, before, requests) - err = Execute(context.Background(), deps, []string{"run", "artifact", "download", "9", "-R", "owner/repo", "-O", "-", "--json"}) - require.ErrorContains(t, err, "binary") - require.Equal(t, before, requests) + err := Execute(context.Background(), deps, []string{"run", "download", "29", "-R", "owner/repo", "--zip", "-n", "build", "-O", "-", "--json"}) + require.ErrorContains(t, err, "run download writes files") } // zipOf returns a ZIP archive of files, by name. diff --git a/internal/app/clone_test.go b/internal/app/clone_test.go index 934176dc..676c2364 100644 --- a/internal/app/clone_test.go +++ b/internal/app/clone_test.go @@ -150,28 +150,6 @@ func TestSSHCloneTrustsAPIEndpointUnlessSSHHostIsConfigured(t *testing.T) { require.Empty(t, runner.calls) } -func TestCloneAddsSourceRemoteForMirrors(t *testing.T) { - original := "" - server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = fmt.Fprintf(w, `{"id":9,"name":"project","full_name":"alice/project","mirror":true,"clone_url":"http://%s/alice/project.git","original_url":%q}`, r.Host, original) - })) - defer server.Close() - deps, out, _ := serverDeps(t, server.URL) - clone := credentialCall(t, deps, server.URL, "alice", "clone", "--", server.URL+"/alice/project.git", "destination") - original = "https://github.com/upstream/project.git" - runner := &fakeGit{results: map[string]string{clone: "", "remote add -- source " + original: ""}} - deps.Git = runner - args := []string{"repo", "clone", "alice/project", "destination", "--no-input", "--json"} - require.NoError(t, Execute(context.Background(), deps, args)) - require.Equal(t, []string{clone, "remote add -- source " + original}, runner.calls) - require.NotContains(t, out.String(), "github.com") - for _, unsafe := range []string{"https://user:secret@github.com/upstream/project.git", "file:///srv/project.git", "ext::sh -c touch% /tmp/pwned"} { - original, runner.calls = unsafe, nil - require.NoError(t, Execute(context.Background(), deps, args), unsafe) - require.Equal(t, []string{clone}, runner.calls, unsafe) - } -} - func TestClonePassesGitFlagsAndNamesTheUpstreamRemote(t *testing.T) { server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, `{"name":"project","full_name":"bob/project","fork":true,"clone_url":"http://%[1]s/bob/project.git","parent":{"full_name":"alice/project","default_branch":"main","clone_url":"http://%[1]s/alice/project.git"}}`, r.Host) diff --git a/internal/app/config.go b/internal/app/config.go index e041e57f..be4f130f 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -107,11 +107,10 @@ func (a *application) effectivePreferences(snapshot settings.Snapshot) (settings return preferences, sources, nil } -// configSetting is one effective setting and where its value comes from. +// configSetting is one effective setting. type configSetting struct { - Key string - Value any - Source string + Key string + Value any } // settingValues returns the effective preferences, the settings of the selected instance when it is @@ -242,45 +241,35 @@ $ tea config get ssh_key --host https://gitea.com --user alice`, } get.Flags().BoolVar(&getSource, "show-source", false, "Also print where the value comes from") group.AddCommand(get) - var listSource bool list := &cobra.Command{ Use: "list", Short: "List settings and their values", Args: argsExact(0), Example: `$ tea config list -# Include the settings of an instance and where each value comes from -$ tea config list --host https://gitea.com --show-source`, +# Include the settings of an instance +$ tea config list --host https://gitea.com`, RunE: func(cmd *cobra.Command, _ []string) error { snapshot, err := a.deps.Settings.Load() if err != nil { return err } - values, sources, err := a.settingValues(snapshot) + values, _, err := a.settingValues(snapshot) if err != nil { return err } if a.options.json != "" { - return a.writeSettingsJSON(cmd, values, sources, listSource) + return a.writeJSON(cmd, values) } items := make([]configSetting, 0, len(values)) for _, key := range slices.Sorted(maps.Keys(values)) { - items = append(items, configSetting{key, values[key], settingSource(sources, key)}) - } - columns := []terminal.Column{{Header: "Key"}, {Header: "Value", Flex: true}} - if listSource { - columns = append(columns, terminal.Column{Header: "Source"}) + items = append(items, configSetting{key, values[key]}) } return writeTable(a, cmd, items, listSpec[configSetting]{ - Columns: columns, + Columns: []terminal.Column{{Header: "Key"}, {Header: "Value", Flex: true}}, Row: func(item configSetting) []terminal.Cell { - row := []terminal.Cell{{Text: item.Key}, {Text: fmt.Sprint(item.Value)}} - if listSource { - row = append(row, terminal.Cell{Text: item.Source, Muted: true}) - } - return row + return []terminal.Cell{{Text: item.Key}, {Text: fmt.Sprint(item.Value)}} }, }, listPage{Number: 1}) }, } - list.Flags().BoolVar(&listSource, "show-source", false, "Show where each value comes from") group.AddCommand(list) group.AddCommand(&cobra.Command{ Use: "set ", Short: "Change a setting", Args: argsExact(2), ValidArgsFunction: a.completeConfig, diff --git a/internal/app/human_output.go b/internal/app/human_output.go index 608aa9d6..62e06077 100644 --- a/internal/app/human_output.go +++ b/internal/app/human_output.go @@ -33,13 +33,12 @@ func (a *application) display() terminal.Display { preferences, _ := a.preferences() streams := a.deps.Streams return terminal.Display{ - TTY: streams.OutTTY, - Color: terminal.ColorEnabled(preferences.Color, streams.OutTTY, a.deps.Getenv), - TrueColor: terminal.TrueColor(a.deps.Getenv), - Links: terminal.HyperlinksEnabled(preferences.Hyperlinks, streams.OutTTY, a.deps.Getenv), - Markdown: terminal.MarkdownEnabled(preferences.Markdown, streams.OutTTY), - Width: streams.Width, - Now: a.deps.Now(), + TTY: streams.OutTTY, + Color: terminal.ColorEnabled(preferences.Color, streams.OutTTY, a.deps.Getenv), + Links: terminal.HyperlinksEnabled(preferences.Hyperlinks, streams.OutTTY, a.deps.Getenv), + Markdown: terminal.MarkdownEnabled(preferences.Markdown, streams.OutTTY), + Width: streams.Width, + Now: a.deps.Now(), } } diff --git a/internal/app/human_output_test.go b/internal/app/human_output_test.go index 20cb98b8..cd423349 100644 --- a/internal/app/human_output_test.go +++ b/internal/app/human_output_test.go @@ -62,12 +62,11 @@ func TestLabelListAppliesColorAndPagesWithNoInput(t *testing.T) { _, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"ff0000","description":"Defect"},{"id":2,"name":"docs","color":"00ff00","description":"Docs"}]`)) })) defer server.Close() - deps, out, diagnostics, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager", "COLORTERM": "truecolor"}, "color", "always") + deps, out, diagnostics, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager"}, "color", "always") require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"})) require.Equal(t, 1, pager.calls) require.Equal(t, "fake-pager", pager.command) - require.Contains(t, pager.input, "\x1b[38;2;255;0;0m● ff0000\x1b[0m") - require.Contains(t, pager.input, "\x1b[38;2;0;255;0m● 00ff00\x1b[0m") + require.Contains(t, pager.input, "\x1b[90mID\x1b[0m") require.Equal(t, "PAGED:"+pager.input, out.String()) require.Empty(t, diagnostics.String()) } @@ -120,7 +119,7 @@ func TestTeaPagerAndColorEnvironmentOverrides(t *testing.T) { deps, out, _, pager := pagedDeps(t, server.URL, env, "pager", "configured-pager", "color", "never") require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"})) require.Equal(t, "tea-pager", pager.command) - require.Contains(t, pager.input, "\x1b[38;5;196m● ff0000\x1b[0m") + require.Contains(t, pager.input, "\x1b[90mID\x1b[0m") out.Reset() require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "pager", "--show-source", "--json"})) require.JSONEq(t, `{"pager":{"value":"tea-pager","source":"TEA_PAGER"}}`, withoutANSI(out.String())) @@ -142,7 +141,7 @@ func TestColorAutoUsesStdoutTTY(t *testing.T) { out.Reset() deps.Streams.OutTTY = true require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"})) - require.Contains(t, out.String(), "\x1b[38;5;196m● ff0000\x1b[0m") + require.Contains(t, out.String(), "\x1b[90mID\x1b[0m") } func TestMultiItemHumanOutputPagesOnce(t *testing.T) { diff --git a/internal/app/label_options_test.go b/internal/app/label_options_test.go index 92a2c3dd..1b120608 100644 --- a/internal/app/label_options_test.go +++ b/internal/app/label_options_test.go @@ -11,7 +11,6 @@ import ( "strings" "testing" - "gitea.dev/tea/internal/terminal" "github.com/stretchr/testify/require" ) @@ -96,15 +95,11 @@ func TestLabelsByName(t *testing.T) { labels := `[{"id":11,"name":"bug","color":"ee0701"},{"id":12,"name":"kind/feature","color":"84b6eb","exclusive":true},{"id":13,"name":"2026","color":"cccccc"}]` server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method + " " + r.URL.Path { - case "GET /api/v1/repos/alice/project/labels", "GET /api/v1/repos/bob/source/labels": + case "GET /api/v1/repos/alice/project/labels": if r.URL.Query().Get("page") != "1" { _, _ = w.Write([]byte(`[]`)) return } - if r.URL.Path == "/api/v1/repos/bob/source/labels" { - _, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"000000"},{"id":2,"name":"docs","color":"0000ff","description":"Docs"}]`)) - return - } _, _ = w.Write([]byte(labels)) default: changed = append(changed, r.Method+" "+r.URL.Path+" "+requestFields(t, r, "name", "color", "description")) @@ -131,15 +126,12 @@ func TestLabelsByName(t *testing.T) { require.NoError(t, run("label", "edit", "2026", "--by-name", "-d", "Year")) require.NoError(t, run("label", "delete", "BUG", "12", "--yes")) require.Equal(t, "✓ Deleted label \"bug\" from alice/project\n✓ Deleted label \"kind/feature\" from alice/project\n", diagnostics.String()) - require.NoError(t, run("label", "clone", "bob/source")) - require.Equal(t, "! Skipped label \"bug\" in alice/project: it exists already\n✓ Created label \"docs\" in alice/project\n", diagnostics.String()) require.Equal(t, []string{ "POST /api/v1/repos/alice/project/labels name=docs color=0000FF description=Docs", "PATCH /api/v1/repos/alice/project/labels/12 name=kind/enhancement color= description=", "PATCH /api/v1/repos/alice/project/labels/13 name= color= description=Year", "DELETE /api/v1/repos/alice/project/labels/11 ", "DELETE /api/v1/repos/alice/project/labels/12 ", - "POST /api/v1/repos/alice/project/labels name=docs color=0000ff description=Docs", }, changed) for _, test := range []struct { args []string @@ -194,45 +186,3 @@ func TestLabelCreatePicksAColorAndPrompts(t *testing.T) { require.Equal(t, "From a prompt", created[1]["description"]) require.Equal(t, []string{"Label name", "Color (hex, e.g. d73a4a; empty = random):", "Description (optional):"}, prompt.asked) } - -// labelFormPrompter answers the label form with edit applied to the current values. -type labelFormPrompter struct { - scriptedPrompter - heading string - edit func(*terminal.LabelFormValues) -} - -func (p *labelFormPrompter) EditLabel(_ context.Context, heading string, current terminal.LabelFormValues) (terminal.LabelFormValues, error) { - p.heading = heading - p.edit(¤t) - return current, nil -} - -func TestLabelEditFormStartsFromTheLabel(t *testing.T) { - var edited string - server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.Method + " " + r.URL.Path { - case "GET /api/v1/repos/alice/project/labels": - if r.URL.Query().Get("page") != "1" { - _, _ = w.Write([]byte(`[]`)) - return - } - _, _ = w.Write([]byte(`[{"id":11,"name":"bug","color":"ee0701","description":"Broken"}]`)) - case "PATCH /api/v1/repos/alice/project/labels/11": - edited = requestFields(t, r, "name", "color", "description", "exclusive") - _, _ = w.Write([]byte(`{"id":11,"name":"defect"}`)) - default: - t.Errorf("unexpected %s %s", r.Method, r.URL.Path) - } - })) - defer server.Close() - deps, _, _ := serverDeps(t, server.URL) - deps.Streams.InTTY, deps.Streams.ErrTTY = true, true - prompt := &labelFormPrompter{edit: func(v *terminal.LabelFormValues) { v.Name, v.Exclusive = "defect", true }} - deps.Prompter = prompt - require.NoError(t, Execute(context.Background(), deps, []string{"label", "edit", "bug", "-R", "alice/project"})) - require.Equal(t, `Editing label "bug" in alice/project`, prompt.heading) - require.Equal(t, "name=defect color=ee0701 description=Broken exclusive=true", edited) - deps.Streams.InTTY, deps.Streams.ErrTTY = false, false - require.ErrorIs(t, Execute(context.Background(), deps, []string{"label", "edit", "bug", "-R", "alice/project"}), errNothingToEdit) -} diff --git a/internal/app/labels.go b/internal/app/labels.go index e9e10aa8..c4e73761 100644 --- a/internal/app/labels.go +++ b/internal/app/labels.go @@ -42,7 +42,7 @@ addressed by name or ID.`} } complete := a.completeScopedLabels(&org) group.AddCommand(a.labelListCommand(scope), a.labelCreateCommand(scope), a.labelEditCommand(scope, complete), a.labelDeleteCommand(scope, complete), - a.labelImportCommand(scope), a.labelCloneCommand(scope), a.labelExportCommand(scope)) + a.labelImportCommand(scope), a.labelExportCommand(scope)) return group } @@ -240,16 +240,14 @@ func (a *application) reportLabel(cmd *cobra.Command, s services.Labels, item se func (a *application) labelEditCommand(scope labelScope, complete cobra.CompletionFunc) *cobra.Command { var fields labelFields var byName bool - cmd := &cobra.Command{Use: "edit